// Copyright (c) 2017, 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.

/// A collection of utility methods used by completion contributors.
library;

import 'package:analysis_server/src/protocol_server.dart' show Location;
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/source/source.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
    as protocol
    show Element, ElementKind;

/// The name of the type `dynamic`;
const _dynamicName = 'dynamic';

/// Sort by relevance first, highest to lowest, and then by the completion
/// alphabetically.
Comparator<CompletionSuggestionBuilder> completionComparator = (a, b) {
  if (a.relevance == b.relevance) {
    return a.completion.compareTo(b.completion);
  }
  return b.relevance.compareTo(a.relevance);
};

String buildClosureParameters(
  FunctionType type, {
  required bool includeTypes,
  required bool includeKeywords,
}) {
  var buffer = StringBuffer();
  buffer.write('(');

  var hasNamed = false;
  var hasOptionalPositional = false;
  var parameters = type.formalParameters;
  var existingNames = parameters.map((p) => p.name).nonNulls.toSet();
  for (var i = 0; i < parameters.length; ++i) {
    var parameter = parameters[i];
    if (i != 0) {
      buffer.write(', ');
    }
    if (parameter.isNamed && !hasNamed) {
      hasNamed = true;
      buffer.write('{');
    } else if (parameter.isOptionalPositional && !hasOptionalPositional) {
      hasOptionalPositional = true;
      buffer.write('[');
    }
    if (includeTypes) {
      buffer.write(parameter.type);
      buffer.write(' ');
    }
    var name = parameter.name ?? '';
    if (name.isEmpty) {
      name = 'p$i';
      var index = 1;
      while (existingNames.contains(name)) {
        name = 'p${i}_$index';
        index++;
      }
    }

    if (includeKeywords && parameter.isRequiredNamed) {
      buffer.write('required ');
    }
    buffer.write(name);
  }

  if (hasNamed) {
    buffer.write('}');
  } else if (hasOptionalPositional) {
    buffer.write(']');
  }

  buffer.write(')');
  return buffer.toString();
}

/// Compute default argument list text and ranges based on the given
/// [requiredParams] and [namedParams].
CompletionDefaultArgumentList computeCompletionDefaultArgumentList(
  Iterable<FormalParameterElement> requiredParams,
  Iterable<FormalParameterElement> namedParams,
) {
  var sb = StringBuffer();
  var ranges = <int>[];

  int offset;

  for (var param in requiredParams) {
    if (sb.isNotEmpty) {
      sb.write(', ');
    }
    offset = sb.length;

    var name = param.displayName;
    sb.write(name);
    ranges.addAll([offset, name.length]);
  }

  for (var param in namedParams) {
    if (param.metadata.hasRequired || param.isRequiredNamed) {
      if (sb.isNotEmpty) {
        sb.write(', ');
      }
      var name = param.displayName;
      sb.write('$name: ');
      offset = sb.length;
      // TODO(pq): fix to use getDefaultStringParameterValue()
      sb.write(name);
      ranges.addAll([offset, name.length]);
    }
  }

  return CompletionDefaultArgumentList(
    text: sb.isNotEmpty ? sb.toString() : null,
    ranges: ranges.isNotEmpty ? ranges : null,
  );
}

/// Create a new protocol Element for inclusion in a completion suggestion.
protocol.Element createLocalElementFromToken(
  Source source,
  protocol.ElementKind kind,
  Token nameToken, {
  String? parameters,
  String? returnType,
  bool isAbstract = false,
  bool isDeprecated = false,
}) {
  var name = nameToken.lexeme;
  // TODO(danrubel): use lineInfo to determine startLine and startColumn
  var location = Location(
    source.fullName,
    nameToken.offset,
    nameToken.length,
    0,
    0,
    endLine: 0,
    endColumn: 0,
  );
  var flags = protocol.Element.makeFlags(
    isAbstract: isAbstract,
    isDeprecated: isDeprecated,
    isPrivate: Identifier.isPrivateName(name),
  );
  return protocol.Element(
    kind,
    name,
    flags,
    location: location,
    parameters: parameters,
    returnType: returnType,
  );
}

/// Return a default argument value for the given [parameter].
DefaultArgument? getDefaultStringParameterValue(
  FormalParameterElement parameter,
  String quote,
) {
  var type = parameter.type;
  if (type is InterfaceType) {
    if (type.isDartCoreList) {
      return DefaultArgument('[]', cursorPosition: 1);
    } else if (type.isDartCoreMap) {
      return DefaultArgument('{}', cursorPosition: 1);
    } else if (type.isDartCoreString) {
      return DefaultArgument('$quote$quote', cursorPosition: 1);
    }
  } else if (type is FunctionType) {
    var params = type.formalParameters.indexed
        .map((r) {
          var (index, parameter) = r;
          var name = parameter.name ?? 'p${index + 1}';
          return '${getTypeString(parameter.type)}$name';
        })
        .join(', ');
    // TODO(devoncarew): Support having this method return text with newlines.
    var text = '($params) {  }';
    return DefaultArgument(text, cursorPosition: text.length - 2);
  }
  return null;
}

String getRequestLineIndent(DartCompletionRequest request) {
  var content = request.content;
  var lineStartOffset = request.offset;
  var notWhitespaceOffset = request.offset;
  for (; lineStartOffset > 0; lineStartOffset--) {
    var char = content.substring(lineStartOffset - 1, lineStartOffset);
    if (char == '\n') {
      break;
    }
    if (char != ' ' && char != '\t') {
      notWhitespaceOffset = lineStartOffset - 1;
    }
  }
  return content.substring(lineStartOffset, notWhitespaceOffset);
}

String getTypeString(DartType type) {
  if (type is DynamicType) {
    return '';
  } else {
    return '${type.getDisplayString()} ';
  }
}

/// Instantiates the given [InterfaceElement]
InterfaceType instantiateInstanceElement(
  InterfaceElement element,
  NeverType neverType,
) {
  var typeParameters = element.typeParameters;
  var typeArguments = const <DartType>[];
  if (typeParameters.isNotEmpty) {
    typeArguments = List.filled(typeParameters.length, neverType);
  }
  return element.instantiate(
    typeArguments: typeArguments,
    nullabilitySuffix: NullabilitySuffix.none,
  );
}

/// Returns whether the [parameter] is part of a constructor for a Flutter
/// `Widget`.
bool isFlutterWidgetParameter(FormalParameterElement parameter) {
  var element = parameter.enclosingElement;
  if (element is ConstructorElement && element.enclosingElement.isWidget) {
    return true;
  }
  return false;
}

/// Return name of the type of the given [identifier], or, if it unresolved, the
/// name of its declared [declaredType].
String? nameForType(SimpleIdentifier identifier, TypeAnnotation? declaredType) {
  // Get the type from the identifier element.
  DartType type;
  var element = identifier.element;
  if (element == null) {
    return _dynamicName;
  } else if (element is FunctionTypedElement) {
    if (element is PropertyAccessorElement && element is SetterElement) {
      return null;
    }
    type = element.returnType;
  } else if (element is TypeAliasElement) {
    var aliasedType = element.aliasedType;
    if (aliasedType is FunctionType) {
      type = aliasedType.returnType;
    } else {
      return null;
    }
  } else if (element is VariableElement) {
    type = element.type;
  } else {
    return null;
  }

  // If the type is unresolved, use the declared type.
  if (type is DynamicType) {
    if (declaredType is NamedType) {
      return declaredType.qualifiedName;
    }
    return _dynamicName;
  }
  return type.getDisplayString();
}

class CompletionDefaultArgumentList {
  final String? text;
  final List<int>? ranges;

  new({required this.text, required this.ranges});
}

/// A tuple of text to insert and an (optional) location for the cursor.
class DefaultArgument {
  /// The text to insert.
  final String text;

  /// An optional location for the cursor, relative to the text's start. This
  /// field can be null.
  final int? cursorPosition;

  new(this.text, {this.cursorPosition});
}
