// 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/lsp_protocol/protocol.dart' hide Element;
import 'package:analysis_server/protocol/protocol_generated.dart'
    hide AnalysisGetNavigationParams;
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
import 'package:analysis_server/src/plugin/result_merger.dart';
import 'package:analysis_server/src/protocol_server.dart' show NavigationTarget;
import 'package:analysis_server/src/utilities/navigation/keyword_navigation_computer.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
import 'package:analyzer_plugin/src/utilities/navigation/navigation_dart.dart';

typedef StaticOptions = Either2<bool, DefinitionOptions>;

class DefinitionHandler
    extends
        LspMessageHandler<
          TextDocumentPositionParams,
          TextDocumentDefinitionResult
        >
    with LspPluginRequestHandlerMixin {
  new(super.server);
  @override
  Method get handlesMessage => Method.textDocument_definition;

  @override
  LspJsonHandler<TextDocumentPositionParams> get jsonHandler =>
      TextDocumentPositionParams.jsonHandler;

  Future<List<AnalysisNavigationParams>> getPluginResults(
    String path,
    int offset,
  ) async {
    // LSP requests must be converted to DAS-protocol requests for compatibility
    // with plugins.
    var requestParams = plugin.AnalysisGetNavigationParams(path, offset, 0);
    var responses = await requestFromPlugins(path, requestParams);

    return responses
        .map(
          (response) =>
              plugin.AnalysisGetNavigationResult.fromResponse(response),
        )
        .map(
          (result) => AnalysisNavigationParams(
            path,
            result.regions,
            result.targets,
            result.files,
          ),
        )
        .toList();
  }

  Future<AnalysisNavigationParams> getServerResult(
    ResolvedUnitResult result,
    String path,
    bool supportsLocationLink,
    int offset,
  ) async {
    var collector = NavigationCollectorImpl();

    computeDartNavigation(
      server.resourceProvider,
      collector,
      result,
      offset,
      0,
    );

    // If there are no results, then try keyword navigation.
    if (collector.regions.isEmpty) {
      var keywordNavigationComputer = KeywordNavigationComputer(
        collector,
        result.libraryFragment,
      );
      keywordNavigationComputer.compute(
        result.unit.nodeCovering(offset: offset),
      );
    }

    if (supportsLocationLink) {
      await _updateTargetsWithCodeLocations(collector);
    }
    collector.createRegions();

    return AnalysisNavigationParams(
      path,
      collector.regions,
      collector.targets,
      collector.files,
    );
  }

  @override
  Future<ErrorOr<TextDocumentDefinitionResult>> handle(
    TextDocumentPositionParams params,
    MessageInfo message,
    CancellationToken token,
  ) async {
    var clientCapabilities = message.clientCapabilities;
    if (clientCapabilities == null) {
      // This should not happen unless a client misbehaves.
      return serverNotInitializedError;
    }

    var supportsLocationLink = clientCapabilities.definitionLocationLink;

    var pos = params.position;
    var path = pathOfDoc(params.textDocument);

    return path.mapResult((path) async {
      // Always prefer a LineInfo from a resolved unit than server.getLineInfo.
      var resolvedUnit = (await requireResolvedUnit(path)).resultOrNull;
      var lineInfo = resolvedUnit?.lineInfo ?? server.getLineInfo(path);

      // If there is no lineInfo, the request cannot be translated from LSP line/col
      // to server offset/length.
      if (lineInfo == null) {
        return success(TextDocumentDefinitionResult.t2(const []));
      }

      var offset = toOffset(lineInfo, pos);

      return offset.mapResult((offset) async {
        var allResults = [
          if (resolvedUnit != null)
            await getServerResult(
              resolvedUnit,
              path,
              supportsLocationLink,
              offset,
            ),
          ...await getPluginResults(path, offset),
        ];

        var merger = ResultMerger();
        var mergedResults = merger.mergeNavigation(allResults);
        var mergedTargets = mergedResults?.targets ?? [];

        if (mergedResults == null) {
          return success(TextDocumentDefinitionResult.t2(const []));
        }

        // Convert and filter the results using the correct type of Location class
        // depending on the client capabilities.
        if (supportsLocationLink) {
          var convertedResults = convert(
            mergedTargets,
            (NavigationTarget target) =>
                _toLocationLink(mergedResults, lineInfo, target),
          ).nonNulls.toList();

          var results = _filterResults(
            convertedResults,
            params.textDocument.uri,
            pos.line,
            (LocationLink element) => element.targetUri,
            (LocationLink element) => element.targetSelectionRange,
          );

          return success(TextDocumentDefinitionResult.t2(results));
        } else {
          var convertedResults = convert(
            mergedTargets,
            (NavigationTarget target) => _toLocation(mergedResults, target),
          ).nonNulls.toList();

          var results = _filterResults(
            convertedResults,
            params.textDocument.uri,
            pos.line,
            (Location element) => element.uri,
            (Location element) => element.range,
          );

          return success(
            TextDocumentDefinitionResult.t1(Definition.t1(results)),
          );
        }
      });
    });
  }

  /// Helper that selects the correct results (filtering out at the same
  /// line/location) generically, handling either type of Location class.
  List<T> _filterResults<T>(
    List<T> results,
    Uri sourceUri,
    int sourceLineNumber,
    Uri Function(T) uriSelector,
    Range Function(T) rangeSelector,
  ) {
    // If we fetch navigation on a keyword like `var`, the results will include
    // both the definition and also the variable name. This will cause the editor
    // to show the user both options unnecessarily (the variable name is always
    // adjacent to the var keyword, so providing navigation to it is not useful).
    // To prevent this, filter the list to only those on different lines (or
    // different files).
    var otherResults = results
        .where(
          (element) =>
              uriSelector(element) != sourceUri ||
              rangeSelector(element).start.line != sourceLineNumber,
        )
        .toList();

    return otherResults.isNotEmpty ? otherResults : results;
  }

  /// Get the location of the code (excluding doc comments) for this fragment.
  Future<({int offset, int length})?> _getCodeLocation(
    Fragment fragment,
  ) async {
    Fragment? codeFragment = fragment;
    // For synthetic getters created for fields, we need to access the associated
    // variable to get the codeOffset/codeLength.
    if (codeFragment is PropertyAccessorFragment &&
        codeFragment.element.isOriginVariable) {
      codeFragment = codeFragment.element.nonSynthetic.firstFragment;
    }

    // For extension types, the primary constructor has a range that covers only
    // the parameters / representation type but we want the whole declaration
    // for the code range because otherwise previews will just show `(int a)`
    // which is not what the user expects.
    if (codeFragment.element.enclosingElement
        case ExtensionTypeElement enclosingElement
        when enclosingElement.primaryConstructor == codeFragment.element) {
      codeFragment = codeFragment.enclosingFragment;
    }

    // Read the main codeOffset from the element. This may include doc comments
    // but will give the correct end position.
    int? codeOffset, codeLength;
    if (codeFragment case FragmentImpl codeFragment) {
      codeOffset = codeFragment.codeOffset;
      codeLength = codeFragment.codeLength;
    }

    if (codeFragment == null || codeOffset == null || codeLength == null) {
      return null;
    }

    // Read the declaration so we can get the offset after the doc comments.
    var declaration = await _parsedDeclaration(codeFragment);
    var node = declaration?.node;

    if (node is VariableDeclaration) {
      // For variables, expand to the variable declaration list if this is the
      // only variable so that the target range can include keywords/type.
      // Don't do this when there are multiple becaues it may include other
      // variables in the range.
      var parent = node.parent;
      if (parent is VariableDeclarationList && parent.variables.length == 1) {
        node = node.parent;
      }
    }

    if (node is AnnotatedNode) {
      var offsetAfterDocs = node.firstTokenAfterCommentAndMetadata.offset;

      // Reduce the length by the difference between the end of docs and the start.
      codeLength -= offsetAfterDocs - codeOffset;
      codeOffset = offsetAfterDocs;
    }

    return (offset: codeOffset, length: codeLength);
  }

  Location? _toLocation(
    AnalysisNavigationParams mergedResults,
    NavigationTarget target,
  ) {
    var targetFilePath = mergedResults.files[target.fileIndex];
    var targetFileUri = uriConverter.toClientUri(targetFilePath);
    var targetLineInfo = server.getLineInfo(targetFilePath);
    return targetLineInfo != null
        ? navigationTargetToLocation(targetFileUri, target, targetLineInfo)
        : null;
  }

  LocationLink? _toLocationLink(
    AnalysisNavigationParams mergedResults,
    LineInfo sourceLineInfo,
    NavigationTarget target,
  ) {
    var region = mergedResults.regions.first;
    var targetFilePath = mergedResults.files[target.fileIndex];
    var targetFileUri = uriConverter.toClientUri(targetFilePath);
    var targetLineInfo = server.getLineInfo(targetFilePath);

    return targetLineInfo != null
        ? navigationTargetToLocationLink(
            region,
            sourceLineInfo,
            targetFileUri,
            target,
            targetLineInfo,
          )
        : null;
  }

  Future<void> _updateTargetsWithCodeLocations(
    NavigationCollectorImpl collector,
  ) async {
    for (var targetToUpdate in collector.targetsToUpdate) {
      var codeLocation = await _getCodeLocation(targetToUpdate.fragment);
      if (codeLocation != null) {
        targetToUpdate.target
          ..codeOffset = codeLocation.offset
          ..codeLength = codeLocation.length;
      }
    }
  }

  static Future<FragmentDeclarationResult?> _parsedDeclaration(
    Fragment fragment,
  ) async {
    var session = fragment.element.session;
    if (session == null) {
      return null;
    }

    var libraryPath =
        fragment.libraryFragment?.element.firstFragment.source.fullName;
    if (libraryPath == null) {
      return null;
    }

    var parsedLibrary = session.getParsedLibrary(libraryPath);
    if (parsedLibrary is! ParsedLibraryResult) {
      return null;
    }

    return parsedLibrary.getFragmentDeclaration(fragment);
  }
}

class DefinitionRegistrations extends FeatureRegistration
    with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
  new(super.info);

  @override
  ToJsonable? get options =>
      TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes);

  @override
  Method get registrationMethod => Method.textDocument_definition;

  @override
  StaticOptions get staticOptions => Either2.t1(true);

  @override
  bool get supportsDynamic => clientDynamic.definition;
}
