// 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 'dart:async';

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/computer/computer_call_hierarchy.dart'
    as call_hierarchy;
import 'package:analysis_server/src/lsp/client_capabilities.dart';
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:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/source/source_range.dart';

typedef StaticOptions =
    Either3<bool, CallHierarchyOptions, CallHierarchyRegistrationOptions>;

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

  @override
  ToJsonable? get options =>
      CallHierarchyRegistrationOptions(documentSelector: dartFiles);

  @override
  Method get registrationMethod => Method.textDocument_prepareCallHierarchy;

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

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

/// A handler for `callHierarchy/incoming` that returns the incoming calls for
/// the target supplied by the client.
class IncomingCallHierarchyHandler
    extends
        _AbstractCallHierarchyCallsHandler<
          CallHierarchyIncomingCallsParams,
          CallHierarchyIncomingCallsResult,
          CallHierarchyIncomingCall
        >
    with _CallHierarchyUtils {
  new(super.server);
  @override
  Method get handlesMessage => Method.callHierarchy_incomingCalls;

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

  @override
  bool get requiresTrustedCaller => false;

  /// Fetches incoming calls from a [call_hierarchy.DartCallHierarchyComputer].
  ///
  /// This method is invoked by the superclass which handles similar logic for
  /// both incoming and outgoing calls.
  @override
  Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
    call_hierarchy.DartCallHierarchyComputer computer,
    call_hierarchy.CallHierarchyItem target,
  ) => computer.findIncomingCalls(target, server.searchEngine);

  /// Handles the request by passing the target item to a shared implementation
  /// in the superclass.
  @override
  Future<ErrorOr<CallHierarchyIncomingCallsResult>> handle(
    CallHierarchyIncomingCallsParams params,
    MessageInfo message,
    CancellationToken token,
  ) async {
    var clientCapabilities = message.clientCapabilities;
    if (clientCapabilities == null) {
      return failure(serverNotInitializedError);
    }

    return handleCalls(clientCapabilities, params.item);
  }

  /// Converts a server [call_hierarchy.CallHierarchyCalls] into the correct LSP
  /// type for incoming calls.
  @override
  CallHierarchyIncomingCall toCall(
    call_hierarchy.CallHierarchyCalls calls, {
    required LineInfo localLineInfo,
    required LineInfo itemLineInfo,
    required Set<SymbolKind> supportedSymbolKinds,
  }) {
    return CallHierarchyIncomingCall(
      from: toLspItem(
        calls.item,
        itemLineInfo,
        supportedSymbolKinds: supportedSymbolKinds,
      ),
      fromRanges: calls.ranges
          // For incoming calls, ranges are in the referenced item so we use
          // itemLineInfo and not localLineInfo (which is for the original
          // target we're collecting calls to).
          .map((call) => sourceRangeToRange(itemLineInfo, call))
          .toList(),
    );
  }
}

/// A handler for `callHierarchy/outgoing` that returns the outgoing calls for
/// the target supplied by the client.
class OutgoingCallHierarchyHandler
    extends
        _AbstractCallHierarchyCallsHandler<
          CallHierarchyOutgoingCallsParams,
          CallHierarchyOutgoingCallsResult,
          CallHierarchyOutgoingCall
        >
    with _CallHierarchyUtils {
  new(super.server);
  @override
  Method get handlesMessage => Method.callHierarchy_outgoingCalls;

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

  @override
  bool get requiresTrustedCaller => false;

  /// Fetches outgoing calls from a [call_hierarchy.DartCallHierarchyComputer].
  ///
  /// This method is invoked by the superclass which handles similar logic for
  /// both incoming and outgoing calls.
  @override
  Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
    call_hierarchy.DartCallHierarchyComputer computer,
    call_hierarchy.CallHierarchyItem target,
  ) => computer.findOutgoingCalls(target);

  /// Handles the request by passing the target item to a shared implementation
  /// in the superclass.
  @override
  Future<ErrorOr<CallHierarchyOutgoingCallsResult>> handle(
    CallHierarchyOutgoingCallsParams params,
    MessageInfo message,
    CancellationToken token,
  ) async {
    var clientCapabilities = message.clientCapabilities;
    if (clientCapabilities == null) {
      return failure(serverNotInitializedError);
    }

    return handleCalls(clientCapabilities, params.item);
  }

  /// Converts a server [call_hierarchy.CallHierarchyCalls] into the correct LSP
  /// type for outgoing calls.
  @override
  CallHierarchyOutgoingCall toCall(
    call_hierarchy.CallHierarchyCalls calls, {
    required LineInfo localLineInfo,
    required LineInfo itemLineInfo,
    required Set<SymbolKind> supportedSymbolKinds,
  }) {
    return CallHierarchyOutgoingCall(
      to: toLspItem(
        calls.item,
        itemLineInfo,
        supportedSymbolKinds: supportedSymbolKinds,
      ),
      fromRanges: calls.ranges
          // For incoming calls, ranges are in original target so we use
          // localLineInfo and not itemLineInfo (which is for call target
          // the outbound call points to).
          .map((call) => sourceRangeToRange(localLineInfo, call))
          .toList(),
    );
  }
}

/// A handler for the initial "prepare" request for starting navigation with
/// Call Hierarchy.
///
/// This handler returns the initial target based on the offset where the
/// feature is invoked. Invocations at call sites will resolve to the respective
/// declarations.
///
/// The target returned by this handler will be sent back to the server for
/// incoming/outgoing calls as the user navigates the call hierarchy in the
/// client.
class PrepareCallHierarchyHandler
    extends
        SharedMessageHandler<
          CallHierarchyPrepareParams,
          TextDocumentPrepareCallHierarchyResult
        >
    with _CallHierarchyUtils {
  new(super.server);
  @override
  Method get handlesMessage => Method.textDocument_prepareCallHierarchy;

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

  @override
  bool get requiresTrustedCaller => false;

  @override
  Future<ErrorOr<TextDocumentPrepareCallHierarchyResult>> handle(
    CallHierarchyPrepareParams params,
    MessageInfo message,
    CancellationToken token,
  ) async {
    if (!isDartDocument(params.textDocument)) {
      return success(const []);
    }

    var clientCapabilities = message.clientCapabilities;
    if (clientCapabilities == null) {
      // This should not happen unless a client misbehaves.
      return serverNotInitializedError;
    }

    var pos = params.position;
    var path = pathOfDoc(params.textDocument);
    var unit = await path.mapResult(requireResolvedUnit);
    var offset = unit.mapResultSync((unit) => toOffset(unit.lineInfo, pos));
    return (unit, offset).mapResults((unit, offset) async {
      var supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
      var computer = call_hierarchy.DartCallHierarchyComputer(unit);
      var target = computer.findTarget(offset);
      if (target == null) {
        return success(null);
      }

      return await _convertTarget(unit.session, target, supportedSymbolKinds);
    });
  }

  /// Converts a server [call_hierarchy.CallHierarchyItem] to the LSP protocol
  /// equivalent.
  Future<ErrorOr<TextDocumentPrepareCallHierarchyResult>> _convertTarget(
    AnalysisSession session,
    call_hierarchy.CallHierarchyItem target,
    Set<SymbolKind> supportedSymbolKinds,
  ) async {
    // Since the target might be in a different file (for example if invoked at
    // a call site), we need to get a consistent LineInfo for the target file
    // for this session.
    var targetFile = session.getFile(target.file);
    if (targetFile is! FileResult) {
      return error(
        ErrorCodes.InternalError,
        'Call Hierarchy target was in an unavailable file: '
        '${target.displayName} in ${target.file}',
      );
    }
    var targetLineInfo = targetFile.lineInfo;

    var item = toLspItem(
      target,
      targetLineInfo,
      supportedSymbolKinds: supportedSymbolKinds,
    );
    return success([item]);
  }
}

/// An abstract base class for incoming and outgoing CallHierarchy handlers
/// which perform largely the same task using different LSP classes.
abstract class _AbstractCallHierarchyCallsHandler<P, R, C>
    extends SharedMessageHandler<P, R>
    with _CallHierarchyUtils {
  new(super.server);

  /// Gets the appropriate types of calls for this handler.
  Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
    call_hierarchy.DartCallHierarchyComputer computer,
    call_hierarchy.CallHierarchyItem target,
  );

  /// Handles a request for incoming or outgoing calls (handled by the concrete
  /// implementation) by delegating fetching and converting calls to the
  /// subclass.
  Future<ErrorOr<List<C>?>> handleCalls(
    LspClientCapabilities clientCapabilities,
    CallHierarchyItem item,
  ) async {
    if (!isDartUri(item.uri)) {
      return success(const []);
    }

    var path = pathOfUri(item.uri);
    var unit = await path.mapResult(requireResolvedUnit);
    return unit.mapResult((unit) async {
      var supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
      var computer = call_hierarchy.DartCallHierarchyComputer(unit);

      // Convert the clients item back to one in the servers format so that we
      // can use it to get incoming/outgoing calls.
      var target = toServerItem(
        item,
        unit.lineInfo,
        supportedSymbolKinds: supportedSymbolKinds,
      );

      if (target == null) {
        return error(
          ErrorCodes.ContentModified,
          'Content was modified since Call Hierarchy node was produced',
        );
      }

      var calls = await getCalls(computer, target);
      var results = _convertCalls(unit, calls, supportedSymbolKinds);
      return success(results);
    });
  }

  /// Converts a server [call_hierarchy.CallHierarchyCalls] to the appropriate
  /// LSP type [C].
  C toCall(
    call_hierarchy.CallHierarchyCalls calls, {
    required LineInfo localLineInfo,
    required LineInfo itemLineInfo,
    required Set<SymbolKind> supportedSymbolKinds,
  });

  C? _convertCall(
    AnalysisSession session,
    LineInfo localLineInfo,
    Map<String, LineInfo?> lineInfoCache,
    call_hierarchy.CallHierarchyCalls calls,
    Set<SymbolKind> supportedSymbolKinds,
  ) {
    var filePath = calls.item.file;
    var itemLineInfo = lineInfoCache.putIfAbsent(filePath, () {
      var file = session.getFile(filePath);
      return file is FileResult ? file.lineInfo : null;
    });
    if (itemLineInfo == null) {
      return null;
    }

    return toCall(
      calls,
      localLineInfo: localLineInfo,
      itemLineInfo: itemLineInfo,
      supportedSymbolKinds: supportedSymbolKinds,
    );
  }

  List<C> _convertCalls(
    ResolvedUnitResult unit,
    List<call_hierarchy.CallHierarchyCalls> calls,
    Set<SymbolKind> supportedSymbolKinds,
  ) {
    var session = unit.session;
    var lineInfoCache = <String, LineInfo?>{};
    var results = convert(
      calls,
      (call_hierarchy.CallHierarchyCalls call) => _convertCall(
        session,
        unit.lineInfo,
        lineInfoCache,
        call,
        supportedSymbolKinds,
      ),
    );
    return results.toList();
  }
}

/// Utility methods used by all Call Hierarchy handlers.
mixin _CallHierarchyUtils on HandlerHelperMixin<AnalysisServer> {
  /// A mapping from server kinds to LSP [SymbolKind]s.
  static const toSymbolKindMapping = {
    call_hierarchy.CallHierarchyKind.class_: SymbolKind.Class,
    call_hierarchy.CallHierarchyKind.constructor: SymbolKind.Constructor,
    call_hierarchy.CallHierarchyKind.extension: SymbolKind.Class,
    call_hierarchy.CallHierarchyKind.file: SymbolKind.File,
    call_hierarchy.CallHierarchyKind.function: SymbolKind.Function,
    call_hierarchy.CallHierarchyKind.method: SymbolKind.Method,
    call_hierarchy.CallHierarchyKind.mixin: SymbolKind.Class,
    call_hierarchy.CallHierarchyKind.property: SymbolKind.Property,
  };

  /// A mapping from LSP [SymbolKind]s to server kinds.
  static final fromSymbolKindMapping = {
    for (var entry in toSymbolKindMapping.entries) entry.value: entry.key,
  };

  /// Converts a [SymbolKind] passed back from the client over LSP to a server
  /// [call_hierarchy.CallHierarchyKind].
  call_hierarchy.CallHierarchyKind fromSymbolKind(SymbolKind kind) {
    var result = fromSymbolKindMapping[kind];
    assert(result != null);

    return result ?? call_hierarchy.CallHierarchyKind.unknown;
  }

  /// Converts a server [SourceRange] to an LSP [Range].
  Range sourceRangeToRange(LineInfo lineInfo, SourceRange range) =>
      toRange(lineInfo, range.offset, range.length);

  /// Converts a server [call_hierarchy.CallHierarchyItem] to an LSP
  /// [CallHierarchyItem].
  CallHierarchyItem toLspItem(
    call_hierarchy.CallHierarchyItem item,
    LineInfo lineInfo, {
    required Set<SymbolKind> supportedSymbolKinds,
  }) {
    return CallHierarchyItem(
      name: item.displayName,
      detail: item.containerName,
      kind: toSymbolKind(supportedSymbolKinds, item.kind),
      uri: uriConverter.toClientUri(item.file),
      range: sourceRangeToRange(lineInfo, item.codeRange),
      selectionRange: sourceRangeToRange(lineInfo, item.nameRange),
    );
  }

  /// Converts an LSP [CallHierarchyItem] supplied by the client back to a
  /// server [call_hierarchy.CallHierarchyItem] to use to look up calls.
  ///
  /// Returns `null` if the supplied item is no longer valid (for example its
  /// ranges are no longer valid in the current state of the document).
  call_hierarchy.CallHierarchyItem? toServerItem(
    CallHierarchyItem item,
    LineInfo lineInfo, {
    required Set<SymbolKind> supportedSymbolKinds,
  }) {
    var nameRange = toSourceRange(lineInfo, item.selectionRange);
    var codeRange = toSourceRange(lineInfo, item.range);

    return (nameRange, codeRange).mapResultsSync((nameRange, codeRange) {
      return success(
        call_hierarchy.CallHierarchyItem(
          displayName: item.name,
          containerName: item.detail,
          kind: fromSymbolKind(item.kind),
          file: uriConverter.fromClientUri(item.uri),
          nameRange: nameRange,
          codeRange: codeRange,
        ),
      );
    }).resultOrNull;
  }

  /// Converts a server [call_hierarchy.CallHierarchyKind] to a [SymbolKind]
  /// used in the LSP Protocol.
  SymbolKind toSymbolKind(
    Set<SymbolKind> supportedSymbolKinds,
    call_hierarchy.CallHierarchyKind kind,
  ) {
    var result = toSymbolKindMapping[kind];
    assert(result != null);

    // Handle fallbacks and not-supported kinds.
    if (!supportedSymbolKinds.contains(result)) {
      if (result == SymbolKind.File) {
        result = SymbolKind.Module;
      } else {
        result = null;
      }
    }

    return result ?? SymbolKind.Obj;
  }
}
