// 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 '../back_end/code_writer.dart';
import 'piece.dart';

/// A variable declaration.
///
/// Used for local variable declaration statements, top-level variable
/// declarations and field declarations. Also used to handle splitting between
/// a function or function type's return type and the rest of the function.
///
/// Typed and untyped variables have slightly different splitting logic.
/// Untyped variables never split after the keyword but do indent subsequent
/// variables:
///
///     var longVariableName = initializer,
///         anotherVariable = anotherInitializer;
///
/// Typed variables can split that way too:
///
///     String longVariableName = initializer,
///         anotherVariable = anotherInitializer;
///
/// But they can also split after the type annotation. When that happens, the
/// variables aren't indented:
///
///     VeryLongTypeName
///     longVariableName = initializer,
///     anotherVariable = anotherInitializer;
final class VariablePiece extends Piece {
  /// Split between each variable in a multiple variable declaration.
  static const State _betweenVariables = State(1);

  /// Split after the type annotation and between each variable.
  static const State _afterType = State(2, cost: 2);

  /// The leading keywords (`var`, `final`, `late`) and optional type
  /// annotation.
  final Piece _header;

  /// Each individual variable being declared.
  final List<Piece> _variables;

  /// Whether the variable declaration has a type annotation.
  final bool _hasType;

  /// Whether we are using the 3.7 style.
  final bool _is3Dot7;

  /// Creates a [VariablePiece].
  ///
  /// The [hasType] parameter should be `true` if the variable declaration has
  /// a type annotation.
  VariablePiece(
    this._header,
    this._variables, {
    required bool hasType,
    required bool is3Dot7,
  }) : _hasType = hasType,
       _is3Dot7 = is3Dot7;

  @override
  List<State> get additionalStates => [
    if (_variables.length > 1) _betweenVariables,
    if (_hasType) _afterType,
  ];

  @override
  Set<Shape> allowedChildShapes(State state, Piece child) {
    // If the variable doesn't allow any other states (because it's just
    // `var x` etc.) then allow any shape. That way, if there's a comment
    // inside, the solver doesn't get confused trying to invalidate the
    // VariablePiece.
    if (!_is3Dot7 && _variables.length == 1 && !_hasType) return Shape.all;

    if (child == _header) {
      return Shape.anyIf(state != State.unsplit);
    } else {
      return Shape.anyIf(_variables.length == 1 || state != State.unsplit);
    }
  }

  @override
  void format(CodeWriter writer, State state) {
    writer.format(_header);

    // If we split at the variables (but not the type), then indent the
    // variables and their initializers.
    if (state == _betweenVariables) writer.pushIndent(Indent.expression);

    // Split after the type annotation.
    writer.splitIf(state == _afterType);

    for (var i = 0; i < _variables.length; i++) {
      // Split between variables.
      if (i > 0) writer.splitIf(state != State.unsplit);

      writer.format(_variables[i]);
    }

    if (state == _betweenVariables) writer.popIndent();
  }

  @override
  void forEachChild(void Function(Piece piece) callback) {
    callback(_header);
    _variables.forEach(callback);
  }

  @override
  String get debugName => 'Var';
}
