// Copyright (c) 2018, 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/computer/computer_documentation.dart';
import 'package:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/element_locator.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';

/// A computer for the signature at the specified offset of a Dart
/// [CompilationUnit].
class DartUnitSignatureComputer {
  final AstNode? _node;
  final int _offset;
  final DocumentationPreference documentationPreference;
  final DartDocumentationComputer _documentationComputer;

  new(
    DartdocDirectiveInfo dartdocInfo,
    CompilationUnit unit,
    this._offset, {
    this.documentationPreference = DocumentationPreference.full,
  }) : _documentationComputer = DartDocumentationComputer(dartdocInfo),
       _node = unit.nodeCovering(offset: _offset);

  bool get offsetIsValid => _node != null;

  /// Returns the computed signature information, maybe `null`.
  SignatureInformation? compute() {
    var argumentAndList = _findArgumentAndList();
    if (argumentAndList == null) {
      return null;
    }
    var (argumentList, argument) = argumentAndList;
    String? name;
    Element? element;
    List<FormalParameterElement>? parameters;
    var parent = argumentList.parent;
    if (parent is MethodInvocation) {
      name = parent.methodName.name;
      element = ElementLocator.locate(parent);
      parameters = element is FunctionTypedElement
          ? element.formalParameters
          : null;
    } else if (parent is InstanceCreationExpression) {
      name = parent.constructorName.type.qualifiedName;
      var constructorName = parent.constructorName.name;
      if (constructorName != null) {
        name += '.${constructorName.name}';
      }
      element = ElementLocator.locate(parent);
      parameters = element is FunctionTypedElement
          ? element.formalParameters
          : null;
    } else if (parent case FunctionExpressionInvocation(
      function: Identifier function,
    )) {
      name = function.name;

      if (function.staticType case FunctionType functionType) {
        // Standard function expression.
        element = function.element;
        parameters = functionType.formalParameters;
      } else if (parent.element case ExecutableElement executableElement) {
        // Callable class instance (where we'll look at the `call` method).
        element = executableElement;
        parameters = executableElement.formalParameters;
      }
    }

    if (name == null || element == null || parameters == null) {
      return null;
    }

    // Try to compute the active parameter so the IDE can highlight it.
    int? activeParameterIndex;
    if (argument case Argument(:var correspondingParameter?)) {
      // If we know the active parameter, use its index.
      activeParameterIndex = parameters.indexOf(correspondingParameter);
    } else if (argument is! NamedArgument) {
      // If we're not a named expression, then we can count how many positional
      // parameters there are before us, and then find the index of the same
      // index positional parameter.
      var positionalArgsToSkip = argumentList.arguments
          .where((argument) => argument is! NamedArgument)
          .takeWhile((argument) => argument.end < _offset)
          .length;
      for (var i = 0; i < parameters.length; i++) {
        if (parameters[i].isPositional) {
          // This is the first positional parameter after our skips, so this is
          // the active parameter.
          if (positionalArgsToSkip == 0) {
            activeParameterIndex = i;
            break;
          }
          positionalArgsToSkip--;
        }
      }
    }

    var dartdoc = _documentationComputer.computePreferred(
      element,
      documentationPreference,
    );

    return SignatureInformation(
      name: name,
      parameters: parameters,
      argumentList: argumentList,
      activeParameterIndex: activeParameterIndex,
      dartdoc: dartdoc,
    );
  }

  /// Return the closest argument list surrounding the [_node] and the node for
  /// the active argument (if there is one).
  (ArgumentList, AstNode?)? _findArgumentAndList() {
    var node = _node;
    while (node != null) {
      // Certain nodes don't make sense to search above for an argument list
      // (for example when inside a function expression).
      if (node is FunctionExpression) {
        return null;
      }

      if (node is ArgumentList) {
        return (node, null);
      }
      if (node.parent case ArgumentList list) {
        return (list, node);
      }

      node = node.parent;
    }
    return null;
  }
}

/// Information about a function signature.
class SignatureInformation {
  /// The name of the function/method.
  final String name;

  /// The parameters for the function/method.
  final List<FormalParameterElement> parameters;

  /// The current argument list at the invocation site.
  final ArgumentList argumentList;

  /// Documentation for the function/method.
  final String? dartdoc;

  /// The index in [parameters] for the parameter that matches where the offset
  /// was in the invocation list.
  ///
  /// This is only supplied when it can be computed. Positional arguments past
  /// the number of positional parameters or named arguments with no matching
  /// name will not be returned.
  final int? activeParameterIndex;

  new({
    required this.name,
    required this.parameters,
    required this.argumentList,
    required this.activeParameterIndex,
    required this.dartdoc,
  });

  AnalysisGetSignatureResult toLegacyProtocol() {
    return AnalysisGetSignatureResult(
      name,
      parameters.map(_convertParam).toList(),
      dartdoc: dartdoc,
    );
  }

  ParameterInfo _convertParam(FormalParameterElement param) {
    return ParameterInfo(
      param.isOptionalNamed
          ? ParameterKind.OPTIONAL_NAMED
          : param.isOptionalPositional
          ? ParameterKind.OPTIONAL_POSITIONAL
          : param.isRequiredNamed
          ? ParameterKind.REQUIRED_NAMED
          : ParameterKind.REQUIRED_POSITIONAL,
      param.displayName,
      param.type.getDisplayString(),
      defaultValue: param.defaultValueCode,
    );
  }
}
