// 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/src/handler/legacy/legacy_handler.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer_plugin/src/utilities/formatter.dart';
import 'package:dart_style/src/dart_formatter.dart';
import 'package:dart_style/src/exceptions.dart';
import 'package:dart_style/src/source_code.dart';
import 'package:pub_semver/pub_semver.dart';

/// The handler for the `edit.formatIfEnabled` request.
class EditFormatIfEnabledHandler extends LegacyHandler {
  /// Initialize a newly created handler to be able to service requests for the
  /// [server].
  new(super.server, super.request, super.cancellationToken, super.performance);

  /// Format the given [file] with the given [languageVersion].
  ///
  /// Throws a [FileSystemException] if the file doesn't exist or can't be read.
  /// Throws a [FormatterException] if the code could not be formatted.
  List<SourceEdit> formatFile(File file, {Version? languageVersion}) {
    // TODO(brianwilkerson): Move this to a superclass when `edit.format` is
    //  implemented by a handler class so the code can be shared.
    var originalContent = file.readAsStringSync();
    var code = SourceCode(originalContent);

    // TODO(dantup): Consider using createFormatter() which takes a result and
    //  correctly applies settings like page_width, trailing_commas, and enabled
    //  experiments.
    var formatter = DartFormatter(
      languageVersion: languageVersion ?? defaultFormatterVersion,
    );

    var formatResult = formatter.formatSource(code);
    var formattedContent = formatResult.text;

    var edits = <SourceEdit>[];
    if (formattedContent != originalContent) {
      // TODO(brianwilkerson): Replace full replacements with smaller, more
      //  targeted edits.
      var edit = SourceEdit(0, originalContent.length, formattedContent);
      edits.add(edit);
    }
    return edits;
  }

  @override
  Future<void> handle() async {
    var params = EditFormatIfEnabledParams.fromRequest(
      request,
      clientUriConverter: server.uriConverter,
    );
    var collection = AnalysisContextCollectionImpl(
      includedPaths: params.directories,
      resourceProvider: server.resourceProvider,
      sdkPath: server.sdkPath,
      withFineDependencies: true,
    );
    var sourceFileEdits = <SourceFileEdit>[];
    for (var context in collection.contexts) {
      await _formatInContext(context, sourceFileEdits);
    }
    sendResult(EditFormatIfEnabledResult(sourceFileEdits));
  }

  /// Format all of the Dart files in the given [context] whose associated
  /// `codeStyleOptions` enable formatting, adding the edits to the list of
  /// [sourceFileEdits].
  Future<void> _formatInContext(
    DriverBasedAnalysisContext context,
    List<SourceFileEdit> sourceFileEdits,
  ) async {
    var pathContext = context.resourceProvider.pathContext;
    for (var filePath in context.contextRoot.analyzedFiles()) {
      // Skip anything but .dart files.
      if (!file_paths.isDart(pathContext, filePath)) continue;
      // TODO(pq): consider optimizing this file (re)creation
      // `analyzedFiles()` creates and disposes of a File object only for us
      // to recreate it here. It would be faster if we could get the files
      // directly from the context root.
      var resource = context.resourceProvider.getResource(filePath);
      if (resource is! File) continue;

      var options = context.getAnalysisOptionsForFile(resource);
      if (options.codeStyleOptions.useFormatter) {
        try {
          var library = await context.driver.getResolvedLibrary(filePath);
          var languageVersion = library.effectiveFormatterVersion;
          var sourceEdits = formatFile(
            resource,
            languageVersion: languageVersion,
          );
          if (sourceEdits.isNotEmpty) {
            sourceFileEdits.add(
              SourceFileEdit(filePath, 0, edits: sourceEdits),
            );
          }
        } catch (exception) {
          // Ignore files that can't be formatted.
        }
      }
    }
  }
}
