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

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/computer/computer_highlights.dart';
import 'package:analysis_server/src/lsp/constants.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:analysis_server/src/lsp/semantic_tokens/encoder.dart';
import 'package:analysis_server/src/lsp/semantic_tokens/legend.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:collection/collection.dart';

typedef StaticOptions =
    Either2<SemanticTokensOptions, SemanticTokensRegistrationOptions>;

abstract class AbstractSemanticTokensHandler<T>
    extends LspMessageHandler<T, SemanticTokens?>
    with LspPluginRequestHandlerMixin {
  new(super.server);

  List<List<HighlightRegion>> getPluginResults(String path) {
    var notificationManager = server.notificationManager;
    return notificationManager.highlights.getResults(path);
  }

  Future<List<SemanticTokenInfo>> getServerResult(
    CompilationUnit unit,
    SourceRange? range,
  ) async {
    var computer = DartUnitHighlightsComputer(unit, range: range);
    return computer.computeSemanticTokens();
  }

  Iterable<SemanticTokenInfo> _filter(
    Iterable<SemanticTokenInfo> tokens,
    SourceRange? range,
  ) {
    if (range == null) {
      return tokens;
    }

    return tokens.where(
      (token) =>
          !(token.offset + token.length < range.offset ||
              token.offset > range.end),
    );
  }

  Future<ErrorOr<SemanticTokens?>> _handleImpl(
    TextDocumentIdentifier textDocument,
    CancellationToken token, {
    Range? range,
  }) {
    var path = pathOfDoc(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(null);
      }

      return toSourceRangeNullable(lineInfo, range).mapResult((range) async {
        var serverTokens = resolvedUnit != null
            ? await getServerResult(resolvedUnit.unit, range)
            : <SemanticTokenInfo>[];
        var pluginHighlightRegions = getPluginResults(path).flattenedToList;

        if (token.isCancellationRequested) {
          return cancelled(token);
        }

        var encoder = SemanticTokenEncoder();
        Iterable<SemanticTokenInfo> pluginTokens = encoder
            .convertHighlightToTokens(pluginHighlightRegions);

        // Plugin tokens are not filtered at source, so need to be filtered here.
        pluginTokens = _filter(pluginTokens, range);

        Iterable<SemanticTokenInfo> tokens = [...serverTokens, ...pluginTokens];

        // Capabilities exist for supporting multiline/overlapping tokens. These
        // could be used if any clients take it up (VS Code does not).
        // - clientCapabilities?.multilineTokenSupport
        // - clientCapabilities?.overlappingTokenSupport
        var allowMultilineTokens = false;
        var allowOverlappingTokens = false;

        // Some of the translation operations and the final encoding require
        // the tokens to be sorted. Do it once here to avoid each method needing
        // to do it itself (resulting in multiple sorts).
        tokens = tokens.toList()
          ..sort(SemanticTokenInfo.offsetLengthPrioritySort);

        if (!allowOverlappingTokens) {
          tokens = encoder.splitOverlappingTokens(tokens);
        }

        if (!allowMultilineTokens) {
          tokens = tokens.expand(
            (token) => encoder.splitMultilineTokens(token, lineInfo),
          );

          // Tokens may need re-filtering after being split up as there may
          // now be tokens outside of the range.
          tokens = _filter(tokens, range);
        }

        var semanticTokens = encoder.encodeTokens(tokens.toList(), lineInfo);

        return success(semanticTokens);
      });
    });
  }
}

class SemanticTokensFullHandler
    extends AbstractSemanticTokensHandler<SemanticTokensParams> {
  new(super.server);

  @override
  Method get handlesMessage => Method.textDocument_semanticTokens_full;

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

  @override
  Future<ErrorOr<SemanticTokens?>> handle(
    SemanticTokensParams params,
    MessageInfo message,
    CancellationToken token,
  ) => _handleImpl(params.textDocument, token);
}

class SemanticTokensRangeHandler
    extends AbstractSemanticTokensHandler<SemanticTokensRangeParams> {
  new(super.server);

  @override
  Method get handlesMessage => Method.textDocument_semanticTokens_range;

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

  @override
  Future<ErrorOr<SemanticTokens?>> handle(
    SemanticTokensRangeParams params,
    MessageInfo message,
    CancellationToken token,
  ) => _handleImpl(params.textDocument, token, range: params.range);
}

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

  @override
  ToJsonable? get options => SemanticTokensRegistrationOptions(
    documentSelector: fullySupportedTypes,
    legend: semanticTokenLegend.lspLegend,
    full: Either2<bool, SemanticTokensFullDelta>.t2(
      SemanticTokensFullDelta(delta: false),
    ),
    range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
  );

  @override
  Method get registrationMethod =>
      CustomMethods.semanticTokenDynamicRegistration;

  @override
  StaticOptions get staticOptions => Either2.t1(
    SemanticTokensOptions(
      legend: semanticTokenLegend.lspLegend,
      full: Either2.t2(SemanticTokensFullDelta(delta: false)),
      range: Either2.t1(true),
    ),
  );

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