// Copyright (c) 2020, 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';
import 'package:analysis_server/src/lsp/client_capabilities.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';

/// Helper for reading client dynamic registrations which may be omitted by the
/// client.
class ClientDynamicRegistrations {
  /// All dynamic registrations supported by the Dart LSP server.
  ///
  /// Anything listed here and supported by the client will not send a static
  /// registration but instead dynamically register (usually only for a subset of
  /// files such as for .dart/pubspec.yaml/etc).
  ///
  /// When adding new capabilities that will be registered dynamically, the
  /// test_dynamicRegistration_XXX tests in `lsp/initialization_test.dart` should
  /// also be updated to ensure no double-registrations.
  static const supported = [
    Method.textDocument_didOpen,
    Method.textDocument_didChange,
    Method.textDocument_didClose,
    Method.textDocument_completion,
    Method.textDocument_hover,
    Method.textDocument_inlayHint,
    Method.textDocument_inlineValue,
    Method.textDocument_signatureHelp,
    Method.textDocument_references,
    Method.textDocument_documentHighlight,
    Method.textDocument_documentColor,
    Method.textDocument_formatting,
    Method.textDocument_onTypeFormatting,
    Method.textDocument_rangeFormatting,
    Method.textDocument_definition,
    Method.textDocument_codeAction,
    Method.textDocument_rename,
    Method.textDocument_foldingRange,
    Method.textDocument_selectionRange,
    Method.textDocument_typeDefinition,
    Method.textDocument_prepareCallHierarchy,
    Method.textDocument_prepareTypeHierarchy,
    // workspace.fileOperations covers all file operation methods but we only
    // support this one.
    Method.workspace_willRenameFiles,
    // Semantic tokens are all registered under a single "method" as the
    // actual methods are controlled by the server capabilities.
    CustomMethods.semanticTokenDynamicRegistration,
  ];
  final ClientCapabilities _capabilities;

  new(this._capabilities);

  bool get callHierarchy =>
      _capabilities.textDocument?.callHierarchy?.dynamicRegistration ?? false;

  bool get codeActions =>
      _capabilities.textDocument?.codeAction?.dynamicRegistration ?? false;

  bool get codeLens =>
      _capabilities.textDocument?.codeLens?.dynamicRegistration ?? false;

  bool get colorProvider =>
      _capabilities.textDocument?.colorProvider?.dynamicRegistration ?? false;

  bool get completion =>
      _capabilities.textDocument?.completion?.dynamicRegistration ?? false;

  bool get definition =>
      _capabilities.textDocument?.definition?.dynamicRegistration ?? false;

  bool get didChangeConfiguration =>
      _capabilities.workspace?.didChangeConfiguration?.dynamicRegistration ??
      false;

  bool get documentHighlights =>
      _capabilities.textDocument?.documentHighlight?.dynamicRegistration ??
      false;

  bool get documentLink =>
      _capabilities.textDocument?.documentLink?.dynamicRegistration ?? false;

  bool get documentSymbol =>
      _capabilities.textDocument?.documentSymbol?.dynamicRegistration ?? false;

  bool get fileOperations =>
      _capabilities.workspace?.fileOperations?.dynamicRegistration ?? false;

  bool get folding =>
      _capabilities.textDocument?.foldingRange?.dynamicRegistration ?? false;

  bool get formatting =>
      _capabilities.textDocument?.formatting?.dynamicRegistration ?? false;

  bool get hover =>
      _capabilities.textDocument?.hover?.dynamicRegistration ?? false;

  bool get implementation =>
      _capabilities.textDocument?.implementation?.dynamicRegistration ?? false;

  bool get inlayHints =>
      _capabilities.textDocument?.inlayHint?.dynamicRegistration ?? false;

  bool get inlineValue =>
      _capabilities.textDocument?.inlineValue?.dynamicRegistration ?? false;

  bool get rangeFormatting =>
      _capabilities.textDocument?.rangeFormatting?.dynamicRegistration ?? false;

  bool get references =>
      _capabilities.textDocument?.references?.dynamicRegistration ?? false;

  bool get rename =>
      _capabilities.textDocument?.rename?.dynamicRegistration ?? false;

  bool get selectionRange =>
      _capabilities.textDocument?.selectionRange?.dynamicRegistration ?? false;

  bool get semanticTokens =>
      _capabilities.textDocument?.semanticTokens?.dynamicRegistration ?? false;

  bool get signatureHelp =>
      _capabilities.textDocument?.signatureHelp?.dynamicRegistration ?? false;

  bool get textSync =>
      _capabilities.textDocument?.synchronization?.dynamicRegistration ?? false;

  bool get typeDefinition =>
      _capabilities.textDocument?.typeDefinition?.dynamicRegistration ?? false;

  bool get typeFormatting =>
      _capabilities.textDocument?.onTypeFormatting?.dynamicRegistration ??
      false;

  bool get typeHierarchy =>
      _capabilities.textDocument?.typeHierarchy?.dynamicRegistration ?? false;
}

class ServerCapabilitiesComputer {
  final LspAnalysisServer _server;

  /// List of current registrations.
  Set<Registration> currentRegistrations = {};

  var _lastRegistrationId = 0;

  new(this._server);

  List<TextDocumentFilterScheme> get pluginTypes => _server
      .pluginManager
      .pluginIsolates
      .expand(
        (isolate) =>
            isolate.currentSession?.interestingFileGlobs ?? const <String>[],
      )
      // All published plugins use something like `*.extension` as
      // interestingFiles. Prefix a `**/` so that the glob matches nested
      // folders as well.
      .map(
        (glob) => TextDocumentFilterScheme(scheme: 'file', pattern: '**/$glob'),
      )
      .toList();

  ServerCapabilities computeServerCapabilities(
    LspClientCapabilities clientCapabilities,
  ) {
    var context = _createRegistrationContext();
    var features = LspFeatures(context);

    return ServerCapabilities(
      textDocumentSync: features.textDocumentSync.staticRegistration,
      callHierarchyProvider: features.callHierarchy.staticRegistration,
      completionProvider: features.completion.staticRegistration,
      hoverProvider: features.hover.staticRegistration,
      signatureHelpProvider: features.signatureHelp.staticRegistration,
      definitionProvider: features.definition.staticRegistration,
      documentLinkProvider: features.documentLink.staticRegistration,
      implementationProvider: features.implementation.staticRegistration,
      referencesProvider: features.references.staticRegistration,
      documentHighlightProvider: features.documentHighlight.staticRegistration,
      documentSymbolProvider: features.documentSymbol.staticRegistration,
      codeActionProvider: features.codeActions.staticRegistration,
      codeLensProvider: features.codeLens.staticRegistration,
      colorProvider: features.colors.staticRegistration,
      documentFormattingProvider: features.format.staticRegistration,
      documentOnTypeFormattingProvider:
          features.formatOnType.staticRegistration,
      documentRangeFormattingProvider: features.formatRange.staticRegistration,
      inlayHintProvider: features.inlayHint.staticRegistration,
      inlineValueProvider: features.inlineValue.staticRegistration,
      renameProvider: features.rename.staticRegistration,
      foldingRangeProvider: features.foldingRange.staticRegistration,
      selectionRangeProvider: features.selectionRange.staticRegistration,
      semanticTokensProvider: features.semanticTokens.staticRegistration,
      typeDefinitionProvider: features.typeDefinition.staticRegistration,
      typeHierarchyProvider: features.typeHierarchy.staticRegistration,
      executeCommandProvider: features.executeCommand.staticRegistration,
      workspaceSymbolProvider: features.workspaceSymbol.staticRegistration,
      workspace: WorkspaceOptions(
        workspaceFolders: WorkspaceFoldersServerCapabilities(
          supported: true,
          changeNotifications: features.changeNotifications.staticRegistration,
        ),
        fileOperations: !context.clientDynamic.fileOperations
            ? FileOperationOptions(
                willRename: features.willRename.staticRegistration,
              )
            : null,
      ),
      experimental: {
        // 'experimental' is a field we can put any arbitrary data that is not
        // part of the spec without fear of clashing with future LSP changes.
        //
        // We use this to signal some custom protocol that we support.
        //
        // Some of these fields are objects where bools could be sufficient to
        // allow for future expansion without potentially breaking clients by
        // changing the data type.

        // Indicate that we support the 'updateDiagnosticInformation'
        // custom request.
        'updateDiagnosticInformation': {},

        // Interactive Forms support.
        'interactiveResolveProvider': {
          // The kinds of interactive resolutions that the server supports.
          // For example, "command" indicates that the server supports resolving
          // `ExecuteCommandParams` interactively through "command/resolve".
          'kinds': ['command'],
        },

        'textDocument': {
          // These properties can be used by the client to know that we support
          // custom methods like `dart/textDocument/augmented`.
          'super': {},
          'augmented': {},
          'augmentation': {},
        },
        // TODO(dantup): Add something to indicate support for connectToDtd here
        //  and also bump the legacy protocol version and record in the
        //  changelog that this is available for LSP-over-Legacy.
      },
    );
  }

  /// If the client supports dynamic registrations we can tell it what methods
  /// we support for which documents. For example, this allows us to ask for
  /// file edits for .dart as well as pubspec.yaml but only get hover/completion
  /// calls for .dart. This functionality may not be supported by the client, in
  /// which case they will use the ServerCapabilities to know which methods we
  /// support and it will be up to them to decide which file types they will
  /// send requests for.
  Future<void> performDynamicRegistration() async {
    var features = LspFeatures(_createRegistrationContext());
    var registrations = <Registration>[];

    // Collect dynamic registrations for all features.
    var dynamicRegistrations = features.allFeatures
        .where((feature) => feature.supportsDynamic)
        .expand((feature) => feature.dynamicRegistrations);
    for (var (method, options) in dynamicRegistrations) {
      registrations.add(
        Registration(
          id: (_lastRegistrationId++).toString(),
          method: method.toString(),
          registerOptions: options,
        ),
      );
    }

    await _applyRegistrations(registrations);
  }

  Future<void> _applyRegistrations(List<Registration> newRegistrations) async {
    // Compute a diff of old and new registrations to send the unregister or
    // another register request. We compare registrations by their methods and
    // the hashcode of their registration options to allow for multiple
    // registrations of a single method.

    String registrationHash(Registration registration) =>
        '${registration.method}${registration.registerOptions.hashCode}';

    var newRegistrationsMap = Map.fromEntries(
      newRegistrations.map((r) => MapEntry(r, registrationHash(r))),
    );
    var newRegistrationsJsons = newRegistrationsMap.values.toSet();
    var currentRegistrationsMap = Map.fromEntries(
      currentRegistrations.map((r) => MapEntry(r, registrationHash(r))),
    );
    var currentRegistrationJsons = currentRegistrationsMap.values.toSet();

    var registrationsToAdd = newRegistrationsMap.entries
        .where((entry) => !currentRegistrationJsons.contains(entry.value))
        .map((entry) => entry.key)
        .toList();

    var registrationsToRemove = currentRegistrationsMap.entries
        .where((entry) => !newRegistrationsJsons.contains(entry.value))
        .map((entry) => entry.key)
        .toList();

    // Update the current list before we start sending requests since we
    // go async.
    currentRegistrations
      ..removeAll(registrationsToRemove)
      ..addAll(registrationsToAdd);

    Future<void>? unregistrationRequest;
    if (registrationsToRemove.isNotEmpty) {
      var unregistrations = registrationsToRemove
          .map((r) => Unregistration(id: r.id, method: r.method))
          .toList();
      // It's important not to await this request here, as we must ensure
      // we cannot re-enter this method until we have sent both the unregister
      // and register requests to the client atomically.
      // https://github.com/dart-lang/sdk/issues/47851#issuecomment-988093109
      unregistrationRequest = _server.sendLspRequest(
        Method.client_unregisterCapability,
        UnregistrationParams(unregisterations: unregistrations),
      );
    }

    Future<void>? registrationRequest;
    // Only send the registration request if we have at least one (since
    // otherwise we don't know that the client supports registerCapability).
    if (registrationsToAdd.isNotEmpty) {
      registrationRequest = _server
          .sendLspRequest(
            Method.client_registerCapability,
            RegistrationParams(registrations: registrationsToAdd),
          )
          .then((registrationResponse) {
            var error = registrationResponse.error;
            if (error != null) {
              _server.logErrorToClient(
                'Failed to register capabilities with client: '
                '(${error.code}) '
                '${error.message}',
              );
            }
          });
    }

    // Only after we have sent both unregistration + registration events may
    // we await them, knowing another "thread" could not have executed this
    // method between them.
    await unregistrationRequest;
    await registrationRequest;
  }

  RegistrationContext _createRegistrationContext() {
    return RegistrationContext(
      clientCapabilities: _server.editorClientCapabilities!,
      clientConfiguration: _server.lspClientConfiguration,
      customDartSchemes: _server.uriConverter.supportedNonFileSchemes,
      dartFilters: [
        for (var scheme in {
          'file',
          ..._server.uriConverter.supportedNonFileSchemes,
        })
          TextDocumentFilterScheme(language: 'dart', scheme: scheme),
      ],
      pluginTypes: pluginTypes,
    );
  }
}
