// 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/lsp/handlers/code_actions/abstract_code_actions_producer.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer_plugin/protocol/protocol.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin;
import 'package:collection/collection.dart';

/// Produces [CodeActionLiteral]s from Plugin fixes and assists.
class PluginCodeActionsProducer extends AbstractCodeActionsProducer {
  final AnalysisDriver? _driver;

  new(
    super.server,
    super.file,
    super.lineInfo, {
    required super.offset,
    required super.length,
    required super.shouldIncludeKind,
    required super.editorCapabilities,
    required super.callerCapabilities,
    required super.allowCodeActionLiterals,
    required super.allowCommands,
    required super.analysisOptions,
    required super.allowSnippets,
  }) : _driver = server.getAnalysisDriver(file.path);

  @override
  String get name => 'PluginActionsComputer';

  @override
  Future<List<CodeActionWithPriority>> getAssistActions({
    OperationPerformance? performance,
  }) async {
    // These assists are only provided as literal CodeActions.
    if (!allowCodeActionLiterals) {
      // TODO(dantup): Support this (via createCodeActionLiteralOrApplyCommand)
      //  this will require plugins to create stable IDs/kinds so we can look
      //  them back up in `applyCodeAction`.
      return [];
    }

    var requestParams = plugin.EditGetAssistsParams(path, offset, length);
    var responses = await _sendPluginRequest(requestParams);

    return responses
        .map((response) => plugin.EditGetAssistsResult.fromResponse(response))
        .expand((response) => response.assists)
        .map(_convertAssist)
        .nonNulls
        .toList();
  }

  @override
  Future<List<CodeActionWithPriority>> getFixActions(
    OperationPerformance? performance,
  ) async {
    // These fixes are only provided as literal CodeActions.
    if (!allowCodeActionLiterals) {
      // TODO(dantup): Support this (via createCodeActionLiteralOrApplyCommand)
      //  this will require plugins to create stable IDs/kinds so we can look
      //  them back up in `applyCodeAction`.
      return [];
    }

    var requestParams = plugin.EditGetFixesParams(path, offset);
    var responses = await _sendPluginRequest(requestParams);

    return responses
        .map((response) => plugin.EditGetFixesResult.fromResponse(response))
        .expand((response) => response.fixes)
        .map(_convertFixes)
        .flattenedToList;
  }

  @override
  Future<List<CodeAction>> getRefactorActions(
    OperationPerformance? performance,
  ) async => [];

  @override
  Future<List<CodeAction>> getSourceActions() async => [];

  CodeActionWithPriority? _convertAssist(
    plugin.PrioritizedSourceChange assist,
  ) {
    var kind = toCodeActionKind(assist.change.id, CodeActionKind.Refactor);
    // TODO(dantup): Find a way to filter these earlier, so we don't
    //  compute fixes we will filter out.
    if (!shouldIncludeKind(kind)) {
      return null;
    }

    return (
      action: CodeAction.t1(
        createCodeActionLiteral(
          assist.change,
          kind,
          'assist from plugin',
          path,
          lineInfo,
        ),
      ),
      priority: assist.priority,
    );
  }

  Iterable<CodeActionWithPriority> _convertFixes(
    plugin.AnalysisErrorFixes fixes,
  ) {
    var diagnostic = pluginToDiagnostic(
      server.uriConverter,
      (_) => lineInfo,
      fixes.error,
      supportedTags: callerSupportedDiagnosticTags,
      clientSupportsCodeDescription: callerSupportsCodeDescription,
    );
    return fixes.fixes.map((fix) {
      var kind = toCodeActionKind(fix.change.id, CodeActionKind.QuickFix);
      // TODO(dantup): Find a way to filter these earlier, so we don't
      //  compute fixes we will filter out.
      if (!shouldIncludeKind(kind)) {
        return null;
      }
      return (
        action: CodeAction.t1(
          createCodeActionLiteral(
            fix.change,
            kind,
            'fix from plugin',
            path,
            lineInfo,
            diagnostic: diagnostic,
          ),
        ),
        priority: fix.priority,
      );
    }).nonNulls;
  }

  Future<List<plugin.Response>> _sendPluginRequest(
    plugin.RequestParams requestParams,
  ) async {
    if (_driver == null) {
      return const [];
    }

    var pluginFutures = server.broadcastRequestToPlugins(
      requestParams,
      _driver,
    );

    return waitForResponses(pluginFutures, requestParameters: requestParams);
  }
}
