// Copyright (c) 2021, 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';

/// A fixed set of ClientCapabilities used for clients that may execute LSP
/// requests without performing standard LSP initialization (such as a DTD
/// client or LSP-over-Legacy).
///
/// These capabilities may affect the behaviour and format of responses so
/// should be as stable as possible to avoid affecting existing callers.
final fixedBasicLspClientCapabilities = LspClientCapabilities(
  ClientCapabilities(
    textDocument: TextDocumentClientCapabilities(
      hover: HoverClientCapabilities(contentFormat: [MarkupKind.Markdown]),
    ),
    workspace: WorkspaceClientCapabilities(
      workspaceEdit: WorkspaceEditClientCapabilities(documentChanges: true),
    ),
  ),
);

/// Wraps the client (editor) capabilities to improve performance.
///
/// Sets transferred as arrays in JSON will be converted to Sets for faster
/// lookups and default values and nulls will be handled here.
class LspClientCapabilities {
  /// If the client does not provide capabilities.completion.completionItemKind.valueSet
  /// then we must never send a kind that's not in this list.
  static final Set<CompletionItemKind> defaultSupportedCompletionKinds = {
    CompletionItemKind.Text,
    CompletionItemKind.Method,
    CompletionItemKind.Function,
    CompletionItemKind.Constructor,
    CompletionItemKind.Field,
    CompletionItemKind.Variable,
    CompletionItemKind.Class,
    CompletionItemKind.Interface,
    CompletionItemKind.Module,
    CompletionItemKind.Property,
    CompletionItemKind.Unit,
    CompletionItemKind.Value,
    CompletionItemKind.Enum,
    CompletionItemKind.Keyword,
    CompletionItemKind.Snippet,
    CompletionItemKind.Color,
    CompletionItemKind.File,
    CompletionItemKind.Reference,
  };

  /// If the client does not provide capabilities.documentSymbol.symbolKind.valueSet
  /// then we must never send a kind that's not in this list.
  static final Set<SymbolKind> defaultSupportedSymbolKinds = {
    SymbolKind.File,
    SymbolKind.Module,
    SymbolKind.Namespace,
    SymbolKind.Package,
    SymbolKind.Class,
    SymbolKind.Method,
    SymbolKind.Property,
    SymbolKind.Field,
    SymbolKind.Constructor,
    SymbolKind.Enum,
    SymbolKind.Interface,
    SymbolKind.Function,
    SymbolKind.Variable,
    SymbolKind.Constant,
    SymbolKind.Str,
    SymbolKind.Number,
    SymbolKind.Boolean,
    SymbolKind.Array,
  };

  final ClientCapabilities raw;
  final bool documentChanges;
  final bool changeAnnotations;
  final bool configuration;
  final bool createResourceOperations;
  final bool renameResourceOperations;
  final bool completionDeprecatedFlag;
  final bool applyEdit;
  final bool workDoneProgress;
  final bool completionSnippets;
  final bool renameValidation;
  final bool literalCodeActions;
  final bool insertReplaceCompletionRanges;
  final bool definitionLocationLink;
  final bool typeDefinitionLocationLink;
  final bool hierarchicalSymbols;
  final bool diagnosticCodeDescription;
  final bool lineFoldingOnly;
  final Set<CodeActionKind> codeActionKinds;
  final Set<CompletionItemTag> completionItemTags;
  final Set<DiagnosticTag> diagnosticTags;
  final Set<MarkupKind>? completionDocumentationFormats;
  final Set<MarkupKind>? signatureHelpDocumentationFormats;
  final Set<MarkupKind>? hoverContentFormats;
  final Set<SymbolKind> documentSymbolKinds;
  final Set<SymbolKind> workspaceSymbolKinds;
  final Set<CompletionItemKind> completionItemKinds;
  final Set<InsertTextMode> completionInsertTextModes;
  final bool completionLabelDetails;
  final bool completionDefaultEditRange;
  final bool completionDefaultTextMode;
  final bool experimentalSnippetTextEdit;
  final Set<String> supportedInteractiveFormInputTypes;
  final bool supportsShowMessageRequest;

  /// A set of commands that exist on the client that the server may call.
  final Set<String> supportedCommands;

  /// User-friendly error messages from parsing the experimental capabilities.
  final List<String> experimentalCapabilitiesErrors;

  factory(ClientCapabilities raw) {
    var workspace = raw.workspace;
    var workspaceEdit = workspace?.workspaceEdit;
    var resourceOperations = workspaceEdit?.resourceOperations;
    var textDocument = raw.textDocument;
    var completion = textDocument?.completion;
    var completionItem = completion?.completionItem;
    var completionList = completion?.completionList;
    var completionDefaults = _listToSet(completionList?.itemDefaults);
    var codeAction = textDocument?.codeAction;
    var codeActionLiteral = codeAction?.codeActionLiteralSupport;
    var documentSymbol = textDocument?.documentSymbol;
    var publishDiagnostics = textDocument?.publishDiagnostics;
    var signatureHelp = textDocument?.signatureHelp;
    var signatureInformation = signatureHelp?.signatureInformation;
    var hover = textDocument?.hover;
    var definition = textDocument?.definition;
    var typeDefinition = textDocument?.typeDefinition;
    var workspaceSymbol = workspace?.symbol;

    var applyEdit = workspace?.applyEdit ?? false;
    var codeActionKinds = _listToSet(
      codeActionLiteral?.codeActionKind.valueSet,
    );
    var completionDeprecatedFlag = completionItem?.deprecatedSupport ?? false;
    var completionDocumentationFormats = _listToNullableSet(
      completionItem?.documentationFormat,
    );
    var completionInsertTextModes = _listToSet(
      completionItem?.insertTextModeSupport?.valueSet,
    );
    var completionItemKinds = _listToSet(
      completion?.completionItemKind?.valueSet,
      defaults: defaultSupportedCompletionKinds,
    );
    var completionLabelDetails = completionItem?.labelDetailsSupport ?? false;
    var completionSnippets = completionItem?.snippetSupport ?? false;
    var completionDefaultEditRange = completionDefaults.contains('editRange');
    var completionDefaultTextMode = completionDefaults.contains(
      'insertTextMode',
    );
    var configuration = workspace?.configuration ?? false;
    var createResourceOperations =
        resourceOperations?.contains(ResourceOperationKind.Create) ?? false;
    var renameResourceOperations =
        resourceOperations?.contains(ResourceOperationKind.Rename) ?? false;
    var definitionLocationLink = definition?.linkSupport ?? false;
    var typeDefinitionLocationLink = typeDefinition?.linkSupport ?? false;
    var completionItemTags = _listToSet(completionItem?.tagSupport?.valueSet);
    var diagnosticTags = _listToSet(publishDiagnostics?.tagSupport?.valueSet);
    var documentChanges = workspaceEdit?.documentChanges ?? false;
    var changeAnnotations = workspaceEdit?.changeAnnotationSupport != null;
    var documentSymbolKinds = _listToSet(
      documentSymbol?.symbolKind?.valueSet,
      defaults: defaultSupportedSymbolKinds,
    );
    var hierarchicalSymbols =
        documentSymbol?.hierarchicalDocumentSymbolSupport ?? false;
    var diagnosticCodeDescription =
        publishDiagnostics?.codeDescriptionSupport ?? false;
    var hoverContentFormats = _listToNullableSet(hover?.contentFormat);
    var insertReplaceCompletionRanges =
        completionItem?.insertReplaceSupport ?? false;
    var lineFoldingOnly = textDocument?.foldingRange?.lineFoldingOnly ?? false;
    var literalCodeActions = codeActionLiteral != null;
    var renameValidation = textDocument?.rename?.prepareSupport ?? false;
    var signatureHelpDocumentationFormats = _listToNullableSet(
      signatureInformation?.documentationFormat,
    );
    var workDoneProgress = raw.window?.workDoneProgress ?? false;
    var workspaceSymbolKinds = _listToSet(
      workspaceSymbol?.symbolKind?.valueSet,
      defaults: defaultSupportedSymbolKinds,
    );

    var experimental = _ExperimentalClientCapabilities.parse(raw.experimental);

    return LspClientCapabilities._(
      raw,
      documentChanges: documentChanges,
      changeAnnotations: changeAnnotations,
      configuration: configuration,
      createResourceOperations: createResourceOperations,
      renameResourceOperations: renameResourceOperations,
      completionDeprecatedFlag: completionDeprecatedFlag,
      applyEdit: applyEdit,
      workDoneProgress: workDoneProgress,
      completionSnippets: completionSnippets,
      renameValidation: renameValidation,
      literalCodeActions: literalCodeActions,
      insertReplaceCompletionRanges: insertReplaceCompletionRanges,
      definitionLocationLink: definitionLocationLink,
      typeDefinitionLocationLink: typeDefinitionLocationLink,
      hierarchicalSymbols: hierarchicalSymbols,
      lineFoldingOnly: lineFoldingOnly,
      diagnosticCodeDescription: diagnosticCodeDescription,
      codeActionKinds: codeActionKinds,
      completionItemTags: completionItemTags,
      diagnosticTags: diagnosticTags,
      completionDocumentationFormats: completionDocumentationFormats,
      signatureHelpDocumentationFormats: signatureHelpDocumentationFormats,
      hoverContentFormats: hoverContentFormats,
      documentSymbolKinds: documentSymbolKinds,
      workspaceSymbolKinds: workspaceSymbolKinds,
      completionItemKinds: completionItemKinds,
      completionInsertTextModes: completionInsertTextModes,
      completionLabelDetails: completionLabelDetails,
      completionDefaultEditRange: completionDefaultEditRange,
      completionDefaultTextMode: completionDefaultTextMode,
      experimentalSnippetTextEdit: experimental.snippetTextEdit,
      supportedInteractiveFormInputTypes:
          experimental.interactiveFormInputTypes,
      supportsShowMessageRequest: experimental.showMessageRequest,
      supportedCommands: experimental.commands,
      experimentalCapabilitiesErrors: experimental.errors,
    );
  }

  new _(
    this.raw, {
    required this.documentChanges,
    required this.changeAnnotations,
    required this.configuration,
    required this.createResourceOperations,
    required this.renameResourceOperations,
    required this.completionDeprecatedFlag,
    required this.applyEdit,
    required this.workDoneProgress,
    required this.completionSnippets,
    required this.renameValidation,
    required this.literalCodeActions,
    required this.insertReplaceCompletionRanges,
    required this.definitionLocationLink,
    required this.typeDefinitionLocationLink,
    required this.hierarchicalSymbols,
    required this.lineFoldingOnly,
    required this.diagnosticCodeDescription,
    required this.codeActionKinds,
    required this.completionItemTags,
    required this.diagnosticTags,
    required this.completionDocumentationFormats,
    required this.signatureHelpDocumentationFormats,
    required this.hoverContentFormats,
    required this.documentSymbolKinds,
    required this.workspaceSymbolKinds,
    required this.completionItemKinds,
    required this.completionInsertTextModes,
    required this.completionLabelDetails,
    required this.completionDefaultEditRange,
    required this.completionDefaultTextMode,
    required this.experimentalSnippetTextEdit,
    required this.supportedInteractiveFormInputTypes,
    required this.supportsShowMessageRequest,
    required this.supportedCommands,
    required this.experimentalCapabilitiesErrors,
  });

  /// Converts a list to a `Set`, returning null if the list is null.
  static Set<T>? _listToNullableSet<T>(List<T>? items) {
    return items != null ? {...items} : null;
  }

  /// Converts a list to a `Set`, returning [defaults] if the list is null.
  ///
  /// If [defaults] is not supplied, will return an empty set.
  static Set<T> _listToSet<T>(List<T>? items, {Set<T> defaults = const {}}) {
    return items != null ? {...items} : defaults;
  }
}

/// A helper for parsing experimental capabilities and collecting any errors
/// because their values do not match the types expected by the server.
class _ExperimentalClientCapabilities {
  /// User-friendly error messages from parsing the experimental capabilities.
  final List<String> errors;

  final bool snippetTextEdit;
  final Set<String> interactiveFormInputTypes;
  final Set<String> commands;
  final bool showMessageRequest;

  new({
    required this.snippetTextEdit,
    required this.interactiveFormInputTypes,
    required this.commands,
    required this.showMessageRequest,
    required this.errors,
  });

  /// Parse the experimental capabilities.
  ///
  /// Unlike the capabilities above the spec doesn't define any types for
  /// these, so we may see types we don't expect (whereas the above would have
  /// failed to deserialize if the types are invalid). So, check the types
  /// carefully and report a warning to the client if something looks wrong.
  ///
  /// Example: https://github.com/dart-lang/sdk/issues/55935
  factory parse(Object? raw) {
    var errors = <String>[];

    /// Helper to ensure [object] is type [T] and otherwise records an error in
    /// [errors] and returns `null`.
    T? expectType<T>(String suffix, Object? object, [String? typeDescription]) {
      if (object is! T) {
        errors.add(
          'ClientCapabilities.experimental$suffix must be a ${typeDescription ?? T}',
        );
        return null;
      }
      return object;
    }

    var expectMap = expectType<Map<String, Object?>?>;
    var expectBool = expectType<bool?>;
    var expectString = expectType<String>;

    /// Helper to expect a nullable list of strings and return them as a set.
    Set<String>? expectNullableStringSet(String name, Object? object) {
      return expectType<List<Object?>?>(
        name,
        object,
        'List<String>?',
      )?.map((item) => expectString('$name[]', item)).nonNulls.toSet();
    }

    var experimental = expectMap('', raw) ?? const {};

    // Snippets.
    var snippetTextEdit = expectBool(
      '.snippetTextEdit',
      experimental['snippetTextEdit'],
    );

    // Interactive Forms.
    var interactiveForms = expectMap(
      '.interactiveResolve',
      experimental['interactiveResolve'],
    );
    var interactiveFormInputTypes = expectNullableStringSet(
      '.interactiveResolve.inputTypes',
      interactiveForms?['inputTypes'],
    );

    // Executable commands.
    var commands = expectNullableStringSet(
      '.commands',
      experimental['commands'],
    );

    /// At the time of writing (2023-02-01) there is no official capability for
    /// supporting 'showMessageRequest' because LSP assumed all clients
    /// supported it.
    ///
    /// This turned out to not be the case, so to avoid sending prompts that
    /// might not be seen, we will only use this functionality if we _know_ the
    /// client supports it via a custom flag in 'experimental' that is passed by
    /// the Dart-Code VS Code extension since version v3.58.0 (2023-01-25).
    var showMessageRequest = expectBool(
      '.supportsWindowShowMessageRequest',
      experimental['supportsWindowShowMessageRequest'],
    );

    return _ExperimentalClientCapabilities(
      snippetTextEdit: snippetTextEdit ?? false,
      interactiveFormInputTypes: interactiveFormInputTypes ?? {},
      commands: commands ?? {},
      showMessageRequest: showMessageRequest ?? false,
      errors: errors,
    );
  }
}
