// Copyright (c) 2022, 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:_fe_analyzer_shared/src/scanner/token.dart';
import 'package:analysis_server/lsp_protocol/protocol.dart' hide Element;
import 'package:analysis_server/src/lsp/client_configuration.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/ast/visitor.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/line_info.dart';
import 'package:path/path.dart' as path;

/// A computer for LSP Inlay Hints.
///
/// Inlay hints are text labels used to show inferred labels such as type and
/// argument names where they are not already explicitly present in the source
/// but are being inferred.
class DartInlayHintComputer {
  final path.Context pathContext;
  final LineInfo _lineInfo;
  final CompilationUnit _unit;
  final List<InlayHint> _hints = [];
  final LspClientInlayHintsConfiguration _config;

  new(
    this.pathContext,
    ResolvedUnitResult result, [
    // This parameter is optional because this class is used internally
    // and does not currently pass config.
    LspClientInlayHintsConfiguration? config,
  ]) : _unit = result.unit,
       _lineInfo = result.lineInfo,
       _config = config ?? LspClientInlayHintsConfiguration(null);

  /// Adds a type hint before [period] showing a label for the type [type].
  void addDotShorthandTypePrefix(Token period, DartType? type) {
    if (type == null) {
      return;
    }

    _addTypePrefix(
      period,
      type,
      _InlayHintKind.dotShorthandType,
      paddingRight: false,
    );
  }

  /// Adds a parameter name hint before [nodeOrToken] showing a the name for
  /// [parameter].
  ///
  /// If the parameter has no name, no hint will be added.
  ///
  /// A colon and padding will be added between the hint and [nodeOrToken]
  /// automatically.
  void addParameterNamePrefix(
    SyntacticEntity nodeOrToken,
    FormalParameterElement parameter, {
    required bool hasLiteralValue,
  }) {
    var name = parameter.name;
    if (name == null || name.isEmpty) {
      return;
    }
    var offset = nodeOrToken.offset;
    var position = toPosition(_lineInfo.getLocation(offset));
    var labelParts = Either2<List<InlayHintLabelPart>, String>.t1([
      InlayHintLabelPart(
        value: '$name:',
        location: _locationForElement(parameter),
      ),
    ]);
    _addHint(
      InlayHint(
        label: labelParts,
        position: position,
        kind: InlayHintKind.Parameter,
        paddingRight: true,
      ),
      hasLiteralValue
          ? _InlayHintKind.parameterNameLiteral
          : _InlayHintKind.parameterNameNonLiteral,
    );
  }

  /// Adds a parameter type hint before [entity] showing a label for the type
  /// [type].
  ///
  /// Padding will be added between the hint and [entity] automatically.
  void addParameterTypePrefix(SyntacticEntity entity, DartType type) {
    _addTypePrefix(
      entity,
      type,
      _InlayHintKind.parameterType,
      paddingRight: true,
    );
  }

  /// Adds a return type hint before [token] showing a label for the type
  /// [type].
  ///
  /// Padding will be added between the hint and [token] automatically.
  void addReturnTypePrefix(Token token, DartType type) {
    _addTypePrefix(token, type, _InlayHintKind.returnType, paddingRight: true);
  }

  /// Adds a type hint for [nodeOrToken] showing a label for type arguments
  /// [types].
  ///
  /// Hints will be added before the node unless [suffix] is `true`.
  ///
  /// If [types] is null or empty, no hints are added.
  void addTypeArguments(
    SyntacticEntity nodeOrToken,
    List<DartType>? types, {
    bool suffix = false,
  }) {
    if (types == null || types.isEmpty) {
      return;
    }

    var offset = suffix ? nodeOrToken.end : nodeOrToken.offset;
    var position = toPosition(_lineInfo.getLocation(offset));
    var labelParts = <InlayHintLabelPart>[];
    _appendTypeArgumentParts(labelParts, types);

    _addHint(
      InlayHint(
        label: Either2<List<InlayHintLabelPart>, String>.t1(
          labelParts.toList(),
        ),
        position: position,
        kind: InlayHintKind.Type,
      ),
      _InlayHintKind.typeArgument,
    );
  }

  /// Adds a variable type hint before [entity] showing a label for the type
  /// [type].
  ///
  /// Padding will be added between the hint and [entity] automatically.
  void addVariableTypePrefix(SyntacticEntity entity, DartType type) {
    _addTypePrefix(
      entity,
      type,
      _InlayHintKind.variableType,
      paddingRight: true,
    );
  }

  List<InlayHint> compute() {
    _unit.accept(_DartInlayHintComputerVisitor(this));
    return _hints;
  }

  /// Adds a Type hint for type arguments of [type] (if it has type arguments).
  void maybeAddTypeArguments(
    Token token,
    DartType? type, {
    bool suffix = false,
  }) {
    if (type is! ParameterizedType) {
      return;
    }

    var typeArgumentTypes = type.typeArguments;
    if (typeArgumentTypes.isEmpty) {
      return;
    }

    addTypeArguments(token, typeArgumentTypes, suffix: suffix);
  }

  /// Record [hint] if the user configuration has [kind] enabled.
  void _addHint(InlayHint hint, _InlayHintKind kind) {
    var isEnabled = switch (kind) {
      _InlayHintKind.dotShorthandType => _config.dotShorthandTypesEnabled,
      _InlayHintKind.parameterNameLiteral =>
        _config.parameterNamesMode != InlayHintsParameterNamesMode.none,
      _InlayHintKind.parameterNameNonLiteral =>
        _config.parameterNamesMode == InlayHintsParameterNamesMode.all,
      _InlayHintKind.returnType => _config.returnTypesEnabled,
      _InlayHintKind.parameterType => _config.parameterTypesEnabled,
      _InlayHintKind.variableType => _config.variableTypesEnabled,
      _InlayHintKind.typeArgument => _config.typeArgumentsEnabled,
    };

    if (isEnabled) {
      _hints.add(hint);
    }
  }

  /// Adds a type hint before [nodeOrToken] showing a label for the type [type].
  ///
  /// If [paddingRight] is `true`, padding will be added to the right of the
  /// type name automatically (which is desired except in the case where the
  /// following character is a `.` such as a dot shorthand).
  void _addTypePrefix(
    SyntacticEntity nodeOrToken,
    DartType type,
    _InlayHintKind inlayHintKind, {
    required bool paddingRight,
  }) {
    var offset = nodeOrToken.offset;
    var position = toPosition(_lineInfo.getLocation(offset));
    var labelParts = <InlayHintLabelPart>[];
    _appendTypePart(labelParts, type);
    _addHint(
      InlayHint(
        label: Either2<List<InlayHintLabelPart>, String>.t1(labelParts),
        position: position,
        kind: InlayHintKind.Type,
        paddingRight: paddingRight,
      ),
      inlayHintKind,
    );
  }

  /// Adds labels to [parts] for each element of [type], formatted as
  /// a record type.
  void _appendRecordParts(List<InlayHintLabelPart> parts, RecordType type) {
    parts.add(InlayHintLabelPart(value: '('));

    var positionalFields = type.positionalFields;
    var namedFields = type.namedFields;
    var fieldCount = positionalFields.length + namedFields.length;
    var index = 0;

    for (var field in positionalFields) {
      _appendTypePart(parts, field.type);
      var isLast = index++ == fieldCount - 1;
      if (!isLast) {
        parts.add(InlayHintLabelPart(value: ', '));
      }
    }
    if (namedFields.isNotEmpty) {
      parts.add(InlayHintLabelPart(value: '{'));
      for (var field in namedFields) {
        _appendTypePart(parts, field.type);
        parts.add(InlayHintLabelPart(value: ' ${field.name}'));
        var isLast = index++ == fieldCount - 1;
        if (!isLast) {
          parts.add(InlayHintLabelPart(value: ', '));
        }
      }
      parts.add(InlayHintLabelPart(value: '}'));
    }

    // Add trailing comma for record types with only one position field.
    if (positionalFields.length == 1 && namedFields.isEmpty) {
      parts.add(InlayHintLabelPart(value: ','));
    }

    parts.add(InlayHintLabelPart(value: ')'));
  }

  /// Adds labels to [parts] for each of [types], formatted as type arguments.
  ///
  /// If any of [types] have their own type arguments, will run recursively.
  ///
  /// `<x1, x2, x...>`
  void _appendTypeArgumentParts(
    List<InlayHintLabelPart> parts,
    List<DartType> types,
  ) {
    parts.add(InlayHintLabelPart(value: '<'));
    for (int i = 0; i < types.length; i++) {
      _appendTypePart(parts, types[i]);
      var isLast = i == types.length - 1;
      if (!isLast) {
        parts.add(InlayHintLabelPart(value: ', '));
      }
    }
    parts.add(InlayHintLabelPart(value: '>'));
  }

  /// Adds a label to [parts] for [type].
  ///
  /// If [type] has type arguments, will run recursively.
  void _appendTypePart(List<InlayHintLabelPart> parts, DartType type) {
    if (type is RecordType) {
      _appendRecordParts(parts, type);
    } else {
      parts.add(
        InlayHintLabelPart(
          // Write type without type args or nullability suffix. Type args need
          // adding as their own parts, and the nullability suffix does after them.
          value: type.element?.name ?? type.getDisplayString(),
          location: _locationForElement(type.element),
        ),
      );
      // Call recursively for any nested type arguments.
      if (type is InterfaceType && type.typeArguments.isNotEmpty) {
        _appendTypeArgumentParts(parts, type.typeArguments);
      }
    }
    // Finally add any nullability suffix.
    switch (type.nullabilitySuffix) {
      case NullabilitySuffix.question:
        parts.add(InlayHintLabelPart(value: '?'));
      default:
    }
  }

  Location? _locationForElement(Element? element) {
    if (element == null) {
      return null;
    }
    var firstFragment = element.firstFragment;
    var nameOffset = firstFragment.nameOffset;
    var libraryFragment = firstFragment.libraryFragment;
    var path = libraryFragment?.source.fullName;
    var lineInfo = libraryFragment?.lineInfo;
    if (path == null || lineInfo == null || nameOffset == null) {
      return null;
    }
    return Location(
      uri: pathContext.toUri(path),
      range: toRange(lineInfo, nameOffset, firstFragment.name?.length ?? 0),
    );
  }
}

/// An AST visitor for [DartInlayHintComputer].
class _DartInlayHintComputerVisitor extends GeneralizingAstVisitor<void> {
  final DartInlayHintComputer _computer;

  new(this._computer);

  @override
  void visitArgumentList(ArgumentList node) {
    for (var argument in node.arguments) {
      if (argument is! NamedArgument) {
        var parameter = argument.correspondingParameter;
        if (parameter != null) {
          _computer.addParameterNamePrefix(
            argument,
            parameter,
            hasLiteralValue: argument is Literal,
          );
        }
      }
    }

    // Call super last, to ensure parameter names are always added before
    // any other hints that may be produced (such as list literal Type hints).
    super.visitArgumentList(node);
  }

  @override
  void visitDeclaredIdentifier(DeclaredIdentifier node) {
    super.visitDeclaredIdentifier(node);

    // Has explicit type.
    if (node.type != null) {
      return;
    }

    var declaration = node.declaredFragment?.element;
    if (declaration is LocalVariableElement) {
      _computer.addVariableTypePrefix(node.name, declaration.type);
    }
  }

  @override
  void visitDeclaredVariablePattern(DeclaredVariablePattern node) {
    super.visitDeclaredVariablePattern(node);

    // Has explicit type.
    if (node.type != null) {
      return;
    }

    var declaration = node.declaredFragment?.element;
    if (declaration != null) {
      _computer.addVariableTypePrefix(node.name, declaration.type);
    }
  }

  @override
  void visitDotShorthandConstructorInvocation(
    DotShorthandConstructorInvocation node,
  ) {
    super.visitDotShorthandConstructorInvocation(node);

    _computer.addDotShorthandTypePrefix(node.period, node.staticType);
  }

  @override
  void visitDotShorthandInvocation(DotShorthandInvocation node) {
    super.visitDotShorthandInvocation(node);

    _computer.addDotShorthandTypePrefix(node.period, node.staticType);
  }

  @override
  void visitDotShorthandPropertyAccess(DotShorthandPropertyAccess node) {
    super.visitDotShorthandPropertyAccess(node);

    _computer.addDotShorthandTypePrefix(node.period, node.staticType);
  }

  @override
  void visitFunctionDeclaration(FunctionDeclaration node) {
    super.visitFunctionDeclaration(node);

    // Has explicit type.
    if (node.returnType != null) {
      return;
    }

    // Don't add "void" for setters.
    if (node.isSetter) {
      return;
    }

    var declaration = node.declaredFragment?.element;
    if (declaration != null) {
      // For getters/setters, the type must come before the property keyword,
      // not the name.
      var token = node.propertyKeyword ?? node.name;
      _computer.addReturnTypePrefix(token, declaration.returnType);
    }
  }

  @override
  void visitInvocationExpression(InvocationExpression node) {
    super.visitInvocationExpression(node);

    // Has explicit type arguments.
    if (node.typeArguments != null) {
      return;
    }

    _computer.addTypeArguments(
      node.argumentList.leftParenthesis,
      node.typeArgumentTypes,
    );
  }

  @override
  void visitListLiteral(ListLiteral node) {
    super.visitListLiteral(node);

    // Has explicit type arguments.
    if (node.typeArguments != null) {
      return;
    }

    var token = node.leftBracket;
    var type = node.staticType;
    _computer.maybeAddTypeArguments(token, type);
  }

  @override
  void visitMethodDeclaration(MethodDeclaration node) {
    super.visitMethodDeclaration(node);

    // Has explicit type.
    if (node.returnType != null) {
      return;
    }

    var declaration = node.declaredFragment?.element;
    if (declaration != null) {
      _computer.addReturnTypePrefix(node.name, declaration.returnType);
    }
  }

  @override
  void visitNamedType(NamedType node) {
    super.visitNamedType(node);

    // Has explicit type arguments.
    if (node.typeArguments != null) {
      return;
    }

    var token = node.endToken;
    var type = node.type;
    _computer.maybeAddTypeArguments(token, type, suffix: true);
  }

  @override
  void visitRegularFormalParameter(RegularFormalParameter node) {
    super.visitRegularFormalParameter(node);

    // Has explicit type.
    if (node.isExplicitlyTyped) {
      return;
    }

    var declaration = node.declaredFragment?.element;
    if (declaration != null) {
      // Prefer to insert before `name` to avoid going before keywords like
      // `required`.
      _computer.addParameterTypePrefix(node.name ?? node, declaration.type);
    }
  }

  @override
  void visitSetOrMapLiteral(SetOrMapLiteral node) {
    super.visitSetOrMapLiteral(node);

    // Has explicit type arguments.
    if (node.typeArguments != null) {
      return;
    }

    var token = node.leftBracket;
    var type = node.staticType;
    _computer.maybeAddTypeArguments(token, type);
  }

  @override
  void visitVariableDeclaration(VariableDeclaration node) {
    super.visitVariableDeclaration(node);

    var parent = node.parent;
    // Unexpected parent or has explicit type.
    if (parent is! VariableDeclarationList || parent.type != null) {
      return;
    }

    var declaration = node.declaredFragment?.element;
    if (declaration != null) {
      _computer.addVariableTypePrefix(node, declaration.type);
    }
  }
}

/// Kinds of inlay hints that we generate.
///
/// Each of these can be mapped into a configuration setting that controls it.
enum _InlayHintKind {
  dotShorthandType,
  parameterNameLiteral,
  parameterNameNonLiteral,
  parameterType,
  returnType,
  typeArgument,
  variableType,
}
