// Copyright (c) 2020, 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:analysis_server/src/services/correction/assist.dart';
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer_plugin/utilities/assist/assist.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';

class ConvertToSetLiteral extends ResolvedCorrectionProducer {
  new({required super.context});

  @override
  CorrectionApplicability get applicability =>
      CorrectionApplicability.automatically;

  @override
  AssistKind get assistKind => DartAssistKind.convertToSetLiteral;

  @override
  FixKind get fixKind => DartFixKind.convertToSetLiteral;

  @override
  FixKind get multiFixKind => DartFixKind.convertToSetLiteralMulti;

  @override
  Future<void> compute(ChangeBuilder builder) async {
    //
    // Check whether this is an invocation of `toSet` on a list literal.
    //
    var invocation = _findInvocationOfToSet();
    if (invocation != null) {
      //
      // Extract the information needed to build the edit.
      //
      var target = invocation.target as ListLiteral;
      var hasTypeArgs = target.typeArguments != null;
      var openRange = range.token(target.leftBracket);
      var closeRange = range.startEnd(target.rightBracket, invocation);
      //
      // Build the change and return the assist.
      //
      await builder.addDartFileEdit(file, (builder) {
        if (hasTypeArgs || _listHasUnambiguousElement(target)) {
          builder.addSimpleReplacement(openRange, '{');
        } else {
          builder.addSimpleReplacement(openRange, '<dynamic>{');
        }
        builder.addSimpleReplacement(closeRange, '}');
      });
      return;
    }
    //
    // Check whether this is one of the constructors defined on `Set`.
    //
    var creation = _findSetCreation();
    if (creation != null) {
      //
      // Extract the information needed to build the edit.
      //
      var name = creation.constructorName.name;
      var constructorTypeArguments =
          creation.constructorName.type.typeArguments;
      TypeArgumentList? elementTypeArguments;
      SourceRange? elementsRange;
      if (name == null) {
        // Handle an invocation of the default constructor `Set()`.
      } else if (name.name == 'from' || name.name == 'of') {
        // Handle an invocation of the constructor `Set.from()` or `Set.of()`.
        var arguments = creation.argumentList.arguments;
        if (arguments.length != 1) {
          return;
        }
        if (arguments[0] is ListLiteral) {
          var elements = arguments[0] as ListLiteral;
          elementTypeArguments = elements.typeArguments;
          elementsRange = range.endStart(
            elements.leftBracket,
            elements.rightBracket,
          );
        } else {
          // TODO(brianwilkerson): Consider handling other iterables. Literal
          //  sets could be treated like lists, and arbitrary iterables by using
          //  a spread.
          return;
        }
      } else {
        // Invocation of an unhandled constructor.
        return;
      }
      //
      // Build the edit.
      //
      await builder.addDartFileEdit(file, (builder) {
        builder.addReplacement(range.node(creation), (builder) {
          if (constructorTypeArguments != null) {
            builder.write(utils.getNodeText(constructorTypeArguments));
          } else if (elementTypeArguments != null) {
            builder.write(utils.getNodeText(elementTypeArguments));
          } else if (!_setWouldBeInferred(creation)) {
            builder.write('<dynamic>');
          }
          builder.write('{');
          if (elementsRange != null) {
            builder.write(utils.getRangeText(elementsRange));
          }
          builder.write('}');
        });
      });
    }
  }

  /// Return the invocation of `List.toSet` that is to be converted, or `null`
  /// if the cursor is not inside a invocation of `List.toSet`.
  MethodInvocation? _findInvocationOfToSet() {
    var invocation = node.thisOrAncestorOfType<MethodInvocation>();
    if (invocation == null ||
        node.offset > invocation.argumentList.offset ||
        invocation.methodName.name != 'toSet' ||
        invocation.target is! ListLiteral) {
      return null;
    }
    return invocation;
  }

  /// Return the invocation of a `Set` constructor that is to be converted, or
  /// `null` if the cursor is not inside the invocation of a constructor.
  InstanceCreationExpression? _findSetCreation() {
    var creation = node.thisOrAncestorOfType<InstanceCreationExpression>();
    if (creation == null) {
      return null;
    }

    if (node.offset > creation.argumentList.offset) {
      return null;
    }

    var type = creation.staticType;
    if (type is! InterfaceType) {
      return null;
    }

    // TODO(brianwilkerson): Consider also accepting uses of LinkedHashSet.
    if (type.element != typeProvider.setElement) {
      return null;
    }
    return creation;
  }

  /// Return `true` if the instance [creation] contains at least one unambiguous
  /// element that would cause a set to be inferred.
  bool _hasUnambiguousElement(InstanceCreationExpression creation) {
    var arguments = creation.argumentList.arguments;
    if (arguments.isEmpty) {
      return false;
    }
    return _listHasUnambiguousElement(arguments[0]);
  }

  /// Return `true` if the [element] is sufficient to lexically make the
  /// enclosing literal a set literal rather than a map.
  bool _isUnambiguousElement(CollectionElement? element) {
    if (element is ForElement) {
      return _isUnambiguousElement(element.body);
    } else if (element is IfElement) {
      return _isUnambiguousElement(element.thenElement) ||
          _isUnambiguousElement(element.elseElement);
    } else if (element is Expression) {
      return true;
    }
    return false;
  }

  /// Return `true` if the given [node] is a list literal whose elements, if
  /// placed inside curly braces, would lexically make the resulting literal a
  /// set literal rather than a map literal.
  bool _listHasUnambiguousElement(AstNode node) {
    if (node is ListLiteral && node.elements.isNotEmpty) {
      for (var element in node.elements) {
        if (_isUnambiguousElement(element)) {
          return true;
        }
      }
    }
    return false;
  }

  /// Return `true` if a set would be inferred if the literal replacing the
  /// instance [creation] did not have explicit type arguments.
  bool _setWouldBeInferred(InstanceCreationExpression creation) {
    var parent = creation.parent!;
    if (parent is VariableDeclaration) {
      var parent2 = parent.parent;
      if (parent2 is VariableDeclarationList) {
        var type = parent2.type?.type;
        if (type is InterfaceType && type.element == typeProvider.setElement) {
          return true;
        }
      }
    } else if (parent.parent is InvocationExpression) {
      var parameterElement = creation.correspondingParameter;
      var type = parameterElement?.type;
      if (type is InterfaceType && type.element == typeProvider.setElement) {
        return true;
      }
    }
    return _hasUnambiguousElement(creation);
  }
}
