// Copyright (c) 2014, 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 'dart:io' as io;

import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
import 'package:analysis_server/src/analytics/analytics_manager.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/context_manager.dart';
import 'package:analysis_server/src/legacy_analysis_server.dart';
import 'package:analysis_server/src/lsp/client_capabilities.dart' as lsp;
import 'package:analysis_server/src/lsp/client_configuration.dart' as lsp;
import 'package:analysis_server/src/lsp/constants.dart' as lsp;
import 'package:analysis_server/src/lsp/error_or.dart' as lsp;
import 'package:analysis_server/src/lsp/handlers/handler_execute_command.dart'
    as lsp;
import 'package:analysis_server/src/lsp/handlers/handler_states.dart' as lsp;
import 'package:analysis_server/src/lsp/handlers/handlers.dart' as lsp;
import 'package:analysis_server/src/plugin/notification_manager.dart';
import 'package:analysis_server/src/plugin/plugin_isolate.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/plugin/plugin_watcher.dart';
import 'package:analysis_server/src/protocol_server.dart'
    as legacy
    show MessageType;
import 'package:analysis_server/src/protocol_server.dart' as server;
import 'package:analysis_server/src/scheduler/message_scheduler.dart';
import 'package:analysis_server/src/scheduler/scheduler_tracking_listener.dart';
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/correction/fix_performance.dart';
import 'package:analysis_server/src/services/correction/refactoring_performance.dart';
import 'package:analysis_server/src/services/dart_tooling_daemon/dtd_services.dart';
import 'package:analysis_server/src/services/perf_witness/perf_witness.dart';
import 'package:analysis_server/src/services/pub/pub_api.dart';
import 'package:analysis_server/src/services/pub/pub_command.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/services/search/element_visitors.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analysis_server/src/services/search/search_engine_internal.dart';
import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manager.dart';
import 'package:analysis_server/src/services/user_prompts/survey_manager.dart';
import 'package:analysis_server/src/services/user_prompts/user_prompts.dart';
import 'package:analysis_server/src/session_logger/session_logger.dart';
import 'package:analysis_server/src/status/performance_logger.dart';
import 'package:analysis_server/src/status/utilities/report_data.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analysis_server/src/utilities/extensions/object.dart';
import 'package:analysis_server/src/utilities/file_string_sink.dart';
import 'package:analysis_server/src/utilities/process.dart';
import 'package:analysis_server/src/utilities/request_statistics.dart';
import 'package:analysis_server/src/utilities/tee_string_sink.dart';
import 'package:analysis_server/src/utilities/timing_byte_store.dart';
import 'package:analysis_server_plugin/src/correction/assist_performance.dart';
import 'package:analysis_server_plugin/src/correction/performance.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/exception/exception.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart' as analysis;
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/file_byte_store.dart'
    show EvictingFileByteStore;
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
import 'package:analyzer/src/dart/analysis/library_graph.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/analysis/results.dart';
import 'package:analyzer/src/dart/analysis/session.dart';
import 'package:analyzer/src/dart/analysis/status.dart' as analysis;
import 'package:analyzer/src/dart/analysis/unlinked_unit_store.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/util/platform_info.dart';
import 'package:analyzer/src/utilities/cancellation.dart';
import 'package:analyzer/src/utilities/extensions/analysis_session.dart';
import 'package:analyzer/src/workspace/basic.dart';
import 'package:analyzer/src/workspace/blaze.dart';
import 'package:analyzer/src/workspace/gn.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer_plugin/protocol/protocol.dart';
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'
    as analyzer_plugin;
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:watcher/watcher.dart';

/// The function for sending `openUri` request to the client.
typedef OpenUriNotificationSender = Future<void> Function(Uri uri);

/// The function for sending prompts to the user and collecting button presses.
typedef UserPromptSender =
    Future<String?> Function(
      MessageType type,
      String message,
      List<String> actionLabels,
      lsp.CancellationToken cancellationToken,
    );

/// Implementations of [AnalysisServer] implement a server that listens
/// on an [AbstractNotificationManager] for analysis messages and process them.
abstract class AnalysisServer {
  /// The options of this server instance.
  AnalysisServerOptions options;

  /// The object through which analytics are to be sent.
  final AnalyticsManager analyticsManager;

  /// The versions of each document known to the server (keyed by path), used to
  /// send back to the client for server-initiated edits so that the client can
  /// ensure they have a matching version of the document before applying them.
  ///
  /// Handlers should prefer to use the `getVersionedDocumentIdentifier` method
  /// which will return a null-versioned identifier if the document version is
  /// not known.
  final Map<String, int> documentVersions = {};

  /// A connection to DTD (the Dart Tooling Daemon) that allows other clients to
  /// call server functionality.
  ///
  /// Initialized by the client through a request that calls [connectToDtd].
  DtdServices? dtd;

  /// The object for managing showing surveys to users and recording their
  /// responses.
  ///
  /// `null` if surveys are not enabled.
  @visibleForTesting
  SurveyManager? surveyManager;

  /// The builder for attachments that should be included into crash reports.
  final CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder;

  /// The [ContextManager] that handles the mapping from analysis roots to
  /// context directories.
  late final ContextManager contextManager;

  /// The object used to manage sending a subset of notifications to the client.
  /// The subset of notifications are those to which plugins may contribute.
  AbstractNotificationManager notificationManager;

  /// A reference to the handler for executing commands.
  ///
  /// This allows the server to call commands itself, such as "Fix All in
  /// Workspace" triggered from the [DartFixPromptManager].
  lsp.ExecuteCommandHandler? executeCommandHandler;

  /// The object used to manage the execution of plugins.
  late PluginManager pluginManager;

  /// The object used to manage the SDK's known to this server.
  final DartSdkManager sdkManager;

  /// The [SearchEngine] for this server.
  late final SearchEngine searchEngine = SearchEngineImpl(driverMap.values);

  /// The optional [ByteStore] to use as [byteStore].
  final ByteStore? providedByteStore;

  late ByteStore byteStore;

  late FileContentCache fileContentCache;

  final UnlinkedUnitStore unlinkedUnitStore = UnlinkedUnitStoreImpl();

  late final analysis.AnalysisDriverScheduler analysisDriverScheduler;

  late StreamSubscription<Object?>? analysisDriverSchedulerEventsSubscription;

  /// The DiagnosticServer for this AnalysisServer. If available, it can be used
  /// to start an http diagnostics server or return the port for an existing
  /// server.
  final DiagnosticServer? diagnosticServer;

  /// Additional diagnostic info from the client editor to include in diagnostic
  /// reports.
  Object? clientDiagnosticInformation;

  /// A [RecentBuffer] of the most recent exceptions encountered by the analysis
  /// server.
  final RecentBuffer<ServerException> exceptions = RecentBuffer(10);

  /// The instrumentation service that is to be used by this analysis server.
  InstrumentationService instrumentationService;

  /// The session logger that is to be used by this analysis server.
  SessionLogger sessionLogger;

  /// Performance information after initial analysis is complete
  /// or `null` if the initial analysis is not yet complete
  ServerPerformance? performanceAfterStartup;

  /// A client for making requests to the pub.dev API.
  final PubApi pubApi;

  /// The performance logger.
  final PerformanceLogger? performanceLogger;

  /// A service for fetching pub.dev package details.
  late final PubPackageService pubPackageService;

  /// The class into which performance information is currently being recorded.
  /// During startup, this will be the same as [performanceDuringStartup]
  /// and after startup is complete, this switches to [performanceAfterStartup].
  late ServerPerformance performance;

  /// Performance information before initial analysis is complete.
  final ServerPerformance performanceDuringStartup = ServerPerformance();

  /// Performance about recent requests.
  final ServerRecentPerformance recentPerformance = ServerRecentPerformance();

  final RequestStatisticsHelper? requestStatistics;

  final PerformanceLog<TeeStringSink> analysisPerformanceLogger =
      PerformanceLog<TeeStringSink>(TeeStringSink());

  /// Manages prompts telling the user about "dart fix".
  late final DartFixPromptManager _dartFixPrompt;

  /// The set of the files that are currently priority.
  final Set<String> priorityFiles = <String>{};

  /// The [ResourceProvider] using which paths are converted into [Resource]s.
  final OverlayResourceProvider resourceProvider;

  /// The [ClientUriConverter] to use to convert between URIs/Paths when
  /// communicating with the client.
  late ClientUriConverter _uriConverter = ClientUriConverter.noop(
    resourceProvider.pathContext,
  );

  /// The next modification stamp for a changed file in the [resourceProvider].
  ///
  /// This value is increased each time it is used and used instead of real
  /// modification stamps. It's seeded with `millisecondsSinceEpoch` to reduce
  /// the chance of colliding with values used in previous analysis sessions if
  /// used as a cache key.
  int overlayModificationStamp = DateTime.now().millisecondsSinceEpoch;

  /// Indicates whether the next time the server completes analysis is the first
  /// one since analysis contexts were built.
  ///
  /// This is useful for triggering things like [_dartFixPrompt] only when
  /// it may be useful to do so (after initial analysis and
  /// "pub get"/"pub upgrades").
  bool _isFirstAnalysisSinceContextsBuilt = true;

  /// A completer that tracks in-progress analysis context rebuilds.
  ///
  /// Starts completed and will be replaced each time a context rebuild starts.
  Completer<void> analysisContextRebuildCompleter = Completer()..complete();

  /// The workspace for rename refactorings.
  late final refactoringWorkspace = RefactoringWorkspace(
    driverMap.values,
    searchEngine,
  );

  /// The paths of files for which [ResolvedUnitResult] was produced since
  /// the last idle state.
  final Set<String> filesResolvedSinceLastIdle = {};

  /// A completer for [lspUninitialized].
  final Completer<void> _lspUninitializedCompleter = Completer<void>();

  /// A scheduler that keeps track of all incoming messages and schedules them
  /// for processing.
  final MessageScheduler messageScheduler;

  /// A [TimingByteStore] that records timings for reads from the byte store.
  TimingByteStore? _timingByteStore;

  /// Whether notifications caused by analysis should be suppressed.
  ///
  /// This is used when an operation is temporarily modifying overlays and does
  /// not want the client to be notified of any analysis happening on the
  /// temporary content.
  bool suppressAnalysisResults = false;

  new(
    this.options,
    this.sdkManager,
    this.diagnosticServer,
    this.analyticsManager,
    this.crashReportingAttachmentsBuilder,
    ResourceProvider baseResourceProvider,
    this.instrumentationService,
    this.sessionLogger,
    http.Client? httpClient,
    ProcessRunner? processRunner,
    this.notificationManager, {
    this.requestStatistics,
    // Disable to avoid using this in unit tests.
    @visibleForTesting bool enableBlazeWatcher = false,
    DartFixPromptManager? dartFixPromptManager,
    this.providedByteStore,
    PluginManager? pluginManager,
    MessageSchedulerListener? messageSchedulerListener,
    this.performanceLogger,
    required bool usePlugins,
    Map<String, String>? environment,
  }) : resourceProvider = OverlayResourceProvider(baseResourceProvider),
       pubApi = PubApi(
         instrumentationService,
         httpClient,
         platform.environment['PUB_HOSTED_URL'],
       ),
       messageScheduler = MessageScheduler(
         listener:
             messageSchedulerListener ??
             SchedulerTrackingListener(analyticsManager, performanceLogger),
       ) {
    messageScheduler.server = this;
    // Set the default URI converter. This uses the resource providers path
    // context (unlike the initialized value) which allows tests to override it.
    uriConverter = ClientUriConverter.noop(baseResourceProvider.pathContext);

    // We can only spawn processes (eg. to run pub commands) when backed by
    // a real file system, otherwise we may try to run commands in folders that
    // don't really exist. If processRunner was supplied, it's likely a mock
    // from a test in which case the pub command should still be created.
    if (baseResourceProvider is PhysicalResourceProvider) {
      processRunner ??= ProcessRunner(environment: environment);
    }
    var disablePubCommandVariable =
        platform.environment[PubCommand.disablePubCommandEnvironmentKey];
    var pubCommand = processRunner != null && disablePubCommandVariable == null
        ? PubCommand(
            instrumentationService,
            resourceProvider.pathContext,
            processRunner,
          )
        : null;

    pubPackageService = PubPackageService(
      instrumentationService,
      baseResourceProvider,
      pubApi,
      pubCommand,
    );
    performance = performanceDuringStartup;

    this.pluginManager = pluginManager ??= PluginManager(
      resourceProvider,
      resourceProvider.byteStorePath,
      sdkManager.defaultSdkDirectory,
      notificationManager,
      instrumentationService,
      sessionLogger,
      processRunner: processRunner ?? ProcessRunner(environment: environment),
    );
    var pluginWatcher = PluginWatcher(
      resourceProvider,
      pluginManager,
      pluginsAreEnabled: usePlugins,
    );

    var logName = options.newAnalysisDriverLog;
    if (logName != null) {
      if (logName == 'stdout') {
        analysisPerformanceLogger.sink.addSink(io.stdout);
      } else if (logName.startsWith('file:')) {
        var path = logName.substring('file:'.length);
        analysisPerformanceLogger.sink.addSink(FileStringSink(path));
      }
    }

    var requestStatistics = this.requestStatistics;
    if (requestStatistics != null) {
      analysisPerformanceLogger.sink.addSink(
        requestStatistics.perfLoggerStringSink,
      );
    }

    byteStore = createByteStore(resourceProvider);
    fileContentCache = FileContentCache(resourceProvider);

    analysisDriverScheduler = analysis.AnalysisDriverScheduler(
      analysisPerformanceLogger,
      driverWatcher: pluginWatcher,
    );

    contextManager = ContextManagerImpl(
      resourceProvider,
      sdkManager,
      options.packagesFile,
      options.enabledExperiments,
      byteStore,
      fileContentCache,
      unlinkedUnitStore,
      analysisPerformanceLogger,
      analysisDriverScheduler,
      instrumentationService,
      sessionLogger,
      enableBlazeWatcher: enableBlazeWatcher,
      withFineDependencies: options.withFineDependencies,
    );

    _dartFixPrompt =
        dartFixPromptManager ??
        DartFixPromptManager(
          this,
          UserPromptPreferences(resourceProvider, instrumentationService),
        );
  }

  /// A [Future] that completes when any in-progress analysis context rebuild
  /// completes.
  ///
  /// If no context rebuild is in progress, will return an already complete
  /// [Future].
  Future<void> get analysisContextsRebuilt =>
      analysisContextRebuildCompleter.future;

  /// A list of timings for the byte store, or `null` if timing is not being
  /// tracked.
  List<ByteStoreTimings>? get byteStoreTimings => _timingByteStore?.timings;

  /// The list of current analysis sessions in all contexts.
  Future<List<AnalysisSessionImpl>> get currentSessions async {
    var sessions = <AnalysisSessionImpl>[];
    for (var driver in driverMap.values) {
      await driver.applyPendingFileChanges();
      sessions.add(driver.currentSession);
    }
    return sessions;
  }

  /// The default line terminator that should be used by the server when there
  /// is no existing EOL to copy.
  String get defaultEol => platform.lineTerminator;

  /// A table mapping [Folder]s to the [AnalysisDriver]s associated with them.
  Map<Folder, analysis.AnalysisDriver> get driverMap =>
      contextManager.driverMap;

  /// The capabilities of the editor that owns this server.
  ///
  /// Request handlers should be careful to use the correct clients capabilities
  /// if they are available to non-editor clients (such as over DTD). The
  /// capabilities of the caller are available in [lsp.MessageInfo] and should
  /// usually be used when computing the results for requests, but if those
  /// requests additionally trigger requests to the editor, those requests to
  /// the editor should consider these capabilities.
  ///
  /// For the legacy server, this set may be a fixed set that is not actually
  /// configured by the client, but matches what legacy protocol editors expect
  /// when using LSP-over-Legacy.
  lsp.LspClientCapabilities? get editorClientCapabilities;

  /// The configuration (user/workspace settings) from the LSP client.
  ///
  /// For the legacy server, this set may be a fixed set that is not controlled
  /// by the client.
  lsp.LspClientConfiguration get lspClientConfiguration;

  /// A [Future] that completes when the LSP server moves into the initialized
  /// state and can handle normal LSP requests.
  ///
  /// Completes with the [lsp.InitializedStateMessageHandler] that is active.
  ///
  /// When the server leaves the initialized state, [lspUninitialized] will
  /// complete.
  FutureOr<lsp.InitializedStateMessageHandler> get lspInitialized;

  /// A [Future] that completes once the server transitions out of an
  /// initialized state.
  Future<void> get lspUninitialized => _lspUninitializedCompleter.future;

  /// Returns the function that can send `openUri` request to the client.
  /// Returns `null` is the client does not support it.
  OpenUriNotificationSender? get openUriNotificationSender;

  /// Returns owners of files.
  OwnedFiles get ownedFiles => contextManager.ownedFiles;

  path.Context get pathContext => resourceProvider.pathContext;

  /// Whether or not the client supports showMessageRequest to show the user
  /// a message and allow them to respond by clicking buttons.
  ///
  /// Callers should use [userPromptSender] instead of checking this directly.
  @protected
  bool get supportsShowMessageRequest;

  /// Return the total time the server's been alive.
  Duration get uptime {
    var start = DateTime.fromMillisecondsSinceEpoch(
      performanceDuringStartup.startTime,
    );
    return DateTime.now().difference(start);
  }

  /// The [ClientUriConverter] to use to convert between URIs/Paths when
  /// communicating with the client.
  ClientUriConverter get uriConverter => _uriConverter;

  /// Sets the [ClientUriConverter] to use to convert between URIs/Paths when
  /// communicating with the client.
  set uriConverter(ClientUriConverter value) {
    notificationManager.uriConverter = _uriConverter = value;
  }

  /// Returns the function for sending prompts to the user and collecting button
  /// presses.
  ///
  /// Returns `null` is the client does not support it.
  UserPromptSender? get userPromptSender =>
      supportsShowMessageRequest ? showUserPrompt : null;

  void afterContextsCreated() {
    _timingByteStore?.newTimings('after contexts created');
    _isFirstAnalysisSinceContextsBuilt = true;
  }

  void afterContextsDestroyed() {}

  /// Broadcast a request built from the given [requestParams] to all of the
  /// plugins that are currently associated with the context root from the given
  /// [driver]. Return a list containing futures that will complete when each of
  /// the plugins have sent a response, or an empty list if no [driver] is
  /// provided.
  Map<PluginIsolate, Future<Response>> broadcastRequestToPlugins(
    analyzer_plugin.RequestParams requestParams,
    analysis.AnalysisDriver? driver,
  ) {
    if (driver == null) {
      return <PluginIsolate, Future<Response>>{};
    }
    return pluginManager.broadcastRequest(
      requestParams,
      contextRoot: driver.analysisContext!.contextRoot,
    );
  }

  /// Display a message that will allow us to enable analytics on the next run.
  void checkAnalytics() {
    var unifiedAnalytics = analyticsManager.analytics;
    var prompt = userPromptSender;
    if (!unifiedAnalytics.shouldShowMessage || prompt == null) {
      return;
    }

    unawaited(
      prompt(MessageType.info, unifiedAnalytics.getConsentMessage, [
        lsp.UserPromptActions.ok,
      ], NotCancelableToken()),
    );
    unifiedAnalytics.clientShowedMessage();
  }

  /// Checks that all [sessions] are still consistent, throwing
  /// [InconsistentAnalysisException] if not.
  void checkConsistency(List<AnalysisSessionImpl> sessions) {
    for (var session in sessions) {
      session.checkConsistency();
    }
  }

  /// Completes [lspUninitialized], signalling that the server has moved out
  /// of a state where it can handle standard LSP requests.
  void completeLspUninitialization() {
    if (!_lspUninitializedCompleter.isCompleted) {
      _lspUninitializedCompleter.complete();
    }
  }

  /// Connects to DTD at [dtdUri].
  ///
  /// If there is already an active connection to DTD or there is an error
  /// connecting, returns an error, otherwise returns `null`.
  Future<lsp.ErrorOr<Null>> connectToDtd(
    Uri dtdUri, {
    bool registerExperimentalHandlers = false,
  }) async {
    switch (dtd?.state) {
      case DtdConnectionState.connecting || DtdConnectionState.connected:
        return lsp.error(
          lsp.ServerErrorCodes.stateError,
          'Server is already connected to DTD',
        );
      case DtdConnectionState.disconnected || DtdConnectionState.error || null:
        var connectResult = await DtdServices.connect(
          this,
          dtdUri,
          registerExperimentalHandlers: registerExperimentalHandlers,
        );
        return connectResult.mapResultSync((dtd) {
          this.dtd = dtd;
          return lsp.success(null);
        });
    }
  }

  /// If the state location can be accessed, return the file byte store,
  /// otherwise return the memory byte store.
  ByteStore createByteStore(ResourceProvider resourceProvider) {
    const M = 1024 * 1024 /*1 MiB*/;
    const G = 1024 * 1024 * 1024 /*1 GiB*/;

    const fileCacheSize = 4 * G;
    const memoryCacheSize = 256 * M;

    if (providedByteStore case var providedByteStore?) {
      return providedByteStore;
    }

    if (options.disableFileByteStore ?? false) {
      return MemoryCachingByteStore(NullByteStore(), memoryCacheSize);
    }

    if (resourceProvider is OverlayResourceProvider) {
      resourceProvider = resourceProvider.baseProvider;
    }
    if (resourceProvider is PhysicalResourceProvider) {
      var stateLocation = resourceProvider.getStateLocation('.analysis-driver');
      if (stateLocation != null) {
        var timingByteStore = _timingByteStore = TimingByteStore(
          EvictingFileByteStore(stateLocation.path, fileCacheSize),
        );
        return MemoryCachingByteStore(timingByteStore, memoryCacheSize);
      }
    }

    return MemoryCachingByteStore(NullByteStore(), memoryCacheSize);
  }

  void enableSurveys() {
    // TODO(dantup): This is currently gated on an LSP flag and should just
    //  be inlined into the constructor once we're happy for it to show up
    //  without a flag and for both protocols.
    surveyManager = SurveyManager(
      this,
      instrumentationService,
      analyticsManager.analytics,
    );
  }

  /// Return an analysis driver to which the file with the given [path] is
  /// added if one exists, otherwise a driver in which the file was analyzed if
  /// one exists, otherwise the first driver, otherwise `null`.
  analysis.AnalysisDriver? getAnalysisDriver(String path) {
    var drivers = driverMap.values.toList();
    if (drivers.isNotEmpty) {
      // Sort the drivers so that more deeply nested contexts will be checked
      // before enclosing contexts.
      drivers.sort((first, second) {
        var firstRoot = first.analysisContext!.contextRoot.root.path;
        var secondRoot = second.analysisContext!.contextRoot.root.path;
        return secondRoot.length - firstRoot.length;
      });
      var driver = drivers.firstWhereOrNull(
        (driver) => driver.analysisContext!.contextRoot.isAnalyzed(path),
      );
      driver ??= drivers.firstWhereOrNull(
        (driver) => driver.fsState.getExistingFromPath(path) != null,
      );
      driver ??= drivers.first;
      return driver;
    }
    return null;
  }

  /// Return an [AnalysisSession] in which the file with the given [path]
  /// should be analyzed, preferring the one in which it is analyzed, then
  /// the one where it is referenced, then the first, otherwise `null`.
  Future<AnalysisSession?> getAnalysisSession(String path) async {
    var analysisDriver = getAnalysisDriver(path);
    if (analysisDriver != null) {
      await analysisDriver.applyPendingFileChanges();
      return analysisDriver.currentSession;
    }
    return null;
  }

  DartdocDirectiveInfo getDartdocDirectiveInfoFor(ResolvedUnitResult result) {
    return getDartdocDirectiveInfoForSession(result.session);
  }

  DartdocDirectiveInfo getDartdocDirectiveInfoForSession(
    AnalysisSession session,
  ) {
    var analysisContext = session.analysisContext;
    analysisContext as DriverBasedAnalysisContext;
    return analysisContext.driver.dartdocDirectiveInfo;
  }

  /// Gets the current version number of a document.
  int? getDocumentVersion(String path) => documentVersions[path];

  /// Return a [Future] that completes with the [Element] at the given
  /// [offset] of the given [file], or with `null` if there is no node at the
  /// [offset] or the node does not have an element.
  Future<Element?> getElementAtOffset(String file, int offset) async {
    var unitResult = await getResolvedUnit(file);
    if (unitResult == null) {
      return null;
    }

    if (!priorityFiles.contains(file)) {
      var fragment = findFragmentByNameOffset(
        unitResult.libraryFragment,
        offset,
      );
      if (fragment != null) {
        return fragment.element;
      }
    }

    var node = unitResult.unit.nodeCovering(offset: offset);
    return node?.getElement();
  }

  /// Return a [LineInfo] for the file with the given [path].
  ///
  /// If the file does not exist or cannot be read, returns `null`.
  ///
  /// This method supports non-Dart files but uses the current content of the
  /// file which may not be the latest analyzed version of the file if it was
  /// recently modified, so using the LineInfo from an analyzed result may be
  /// preferable. This method exists mainly to support plugins which do not
  /// provide us a matching [LineInfo] for the content they used.
  LineInfo? getLineInfo(String path) {
    try {
      // First try from the overlay because it may be more up-to-date than
      // the file state.
      var content = resourceProvider.getFile(path).readAsStringSync();
      return LineInfo.fromContent(content);
    } on FileSystemException {
      // If the file does not exist or cannot be read, try the file state
      // because this could be something like a macro-generated file.
      var result = getAnalysisDriver(path)?.getFileSync(path);
      if (result is FileResult) {
        return result.lineInfo;
      }

      return null;
    }
  }

  /// Return the unresolved unit for the file with the given [path].
  ///
  /// Callers should handle [InconsistentAnalysisException] exceptions that may
  /// occur if a file is modified during this operation.
  Future<ParsedUnitResult?> getParsedUnit(String path) async {
    if (!file_paths.isDart(resourceProvider.pathContext, path)) {
      return null;
    }

    var session = await getAnalysisSession(path);
    if (session == null) {
      return null;
    }

    var result = session.getParsedUnit(path);
    return result is ParsedUnitResult ? result : null;
  }

  /// Return the resolved library for the library containing the file with the
  /// given [path]. The library is analyzed in one of the analysis drivers to
  /// which the file was added, otherwise in the first driver, otherwise `null`
  /// is returned.
  Future<ResolvedLibraryResult?> getResolvedLibrary(String path) async {
    if (!file_paths.isDart(resourceProvider.pathContext, path)) {
      return null;
    }
    var driver = getAnalysisDriver(path);
    if (driver == null) {
      return null;
    }
    try {
      return await driver.currentSession.getResolvedContainingLibrary(path);
    } on InconsistentAnalysisException {
      return null;
    } catch (exception, stackTrace) {
      instrumentationService.logException(exception, stackTrace);
    }
    return null;
  }

  /// Return the resolved unit for the file with the given [path]. The file is
  /// analyzed in one of the analysis drivers to which the file was added,
  /// otherwise in the first driver, otherwise `null` is returned.
  Future<ResolvedUnitResult?>? getResolvedUnit(
    String path, {
    bool sendCachedToStream = false,
    bool interactive = true,
  }) {
    if (!file_paths.isDart(resourceProvider.pathContext, path)) {
      return null;
    }

    var driver = getAnalysisDriver(path);
    if (driver == null) {
      return Future.value();
    }

    return driver
        .getResolvedUnit(
          path,
          sendCachedToStream: sendCachedToStream,
          interactive: interactive,
        )
        .then((value) => value is ResolvedUnitResult ? value : null)
        .catchError((Object e, StackTrace st) {
          instrumentationService.logException(e, st);
          return null;
        });
  }

  /// Gets the version of a document known to the server, returning a
  /// [lsp.OptionalVersionedTextDocumentIdentifier] with a version of `null` if the
  /// document version is not known.
  lsp.OptionalVersionedTextDocumentIdentifier getVersionedDocumentIdentifier(
    String path,
  ) {
    return lsp.OptionalVersionedTextDocumentIdentifier(
      uri: uriConverter.toClientUri(path),
      version: getDocumentVersion(path),
    );
  }

  @mustCallSuper
  void handleAnalysisEvent(Object event) {
    switch (event) {
      case FileResult():
        contextManager.callbacks.handleFileResult(event);
      case analysis.AnalysisStatus():
        handleAnalysisStatusChange(event);
    }
  }

  @mustCallSuper
  FutureOr<void> handleAnalysisStatusChange(analysis.AnalysisStatus status) {
    if (_isFirstAnalysisSinceContextsBuilt && !status.isWorking) {
      _timingByteStore?.newTimings('initial analysis completed');
      _isFirstAnalysisSinceContextsBuilt = false;
      _dartFixPrompt.triggerCheck();
    }
    analyticsManager.analysisStatusChanged(
      isWorking: status.isWorking,
      statistics: status
          .ifTypeOrNull<analysis.AnalysisStatusIdle>()
          ?.workingStatistics,
    );
  }

  /// Immediately handles an LSP message by delegating to the
  /// [lsp.InitializedStateMessageHandler]. This method does not schedule the
  /// message and is intended to be used by the scheduler (or LSP-over-Legacy
  /// where the original legacy wrapper was already scheduled).
  ///
  /// If the LSP server/support is not yet initialized, will wait until it is.
  FutureOr<lsp.ErrorOr<Object?>> immediatelyHandleLspMessage(
    lsp.IncomingMessage message,
    lsp.MessageInfo messageInfo, {
    lsp.CancellationToken? cancellationToken,
  }) async {
    // This is FutureOr<> because for the legacy server it's never a future, so
    // we can skip the await.
    var initializedLspHandler = lspInitialized;
    var handler = initializedLspHandler is lsp.InitializedStateMessageHandler
        ? initializedLspHandler
        : await initializedLspHandler;

    return handler.handleMessage(
      message,
      messageInfo,
      cancellationToken: cancellationToken,
    );
  }

  /// Return `true` if the file or directory with the given [path] will be
  /// analyzed in one of the analysis contexts.
  bool isAnalyzed(String path) {
    return contextManager.isAnalyzed(path);
  }

  void logExceptionResult(analysis.ExceptionResult result) {
    var message = 'Analysis failed: ${result.filePath}';
    if (result.contextKey != null) {
      message += ' context: ${result.contextKey}';
    }

    var attachments = crashReportingAttachmentsBuilder.forExceptionResult(
      result,
    );

    // TODO(39284): should this exception be silent?
    instrumentationService.logException(
      SilentException.wrapInMessage(message, result.exception),
      null,
      attachments,
    );
  }

  /// Notify the flutter widget properties support that the file with the
  /// given [path] was changed - added, updated, or removed.
  void notifyFlutterWidgetDescriptions(String path) {}

  /// Prevents the scheduler from processing new messages until [operation]
  /// completes.
  ///
  /// This can be used to obtain analysis results/resolved units consistent with
  /// the state of a file at the time this method was called, preventing
  /// changes by incoming file modifications.
  ///
  /// The contents of [operation] should be kept as short as possible and since
  /// cancellation requests will also be blocked for the duration of this
  /// operation, handlers should generally check the cancellation flag
  /// immediately after this function returns.
  Future<T> pauseSchedulerWhile<T>(FutureOr<T> Function() operation) async {
    // TODO(dantup): Prevent this method from locking responses from the client
    //  because this can lead to deadlocks if called during initialization where
    //  the server may wait for something (configuration) from the client. This
    //  might fit in with potential upcoming scheduler changes.
    //
    // This is currently used by Completion+FixAll (which are less likely, but
    // possible to be called during init).
    //
    // https://github.com/dart-lang/sdk/issues/56311#issuecomment-2250089185

    messageScheduler.pause();
    try {
      // `await` here is important to ensure `finally` doesn't execute until
      // `operation()` completes (`whenComplete` is not available on
      // `FutureOr`).
      return await operation();
    } finally {
      messageScheduler.resume();
    }
  }

  /// Read all files, resolve all URIs, and perform required analysis in
  /// all current analysis drivers.
  Future<void> reanalyze() async {
    await contextManager.refresh();
  }

  /// Report analytics data related to the number and size of files that were
  /// analyzed.
  void reportAnalysisAnalytics() {
    if (performanceLogger != null) {
      Map<String, Object?> collectedData = {};
      collectContextData(collectedData, this);
      performanceLogger!.logMap(collectedData);
    }
    if (!analyticsManager.needsAnslysisCompleteCall()) {
      return;
    }
    var packagesFileMap = <String, File?>{};
    var immediateFileCount = 0;
    var immediateFileLineCount = 0;
    var transitiveFileCount = 0;
    var transitiveFileLineCount = 0;
    var transitiveFilePaths = <String>{};
    var transitiveFileUniqueLineCount = 0;
    var libraryCycles = <LibraryCycle>{};
    var workspaceTypes = List<int>.filled(5, 0);
    var numberOfPackages = <int>[];
    var driverMap = contextManager.driverMap;
    for (var entry in driverMap.entries) {
      var rootPath = entry.key.path;
      var driver = entry.value;
      var analysisContext = driver.analysisContext;
      if (analysisContext != null) {
        var contextRoot = analysisContext.contextRoot;
        packagesFileMap[rootPath] = contextRoot.packagesFile;
        var workspace = contextRoot.workspace;
        switch (workspace) {
          case PackageConfigWorkspace():
            if (workspace.isPubWorkspace) {
              // Pub workspace index = 2, Package workspace index = 1
              workspaceTypes[2]++;
              numberOfPackages.add(workspace.allPackages.length);
            } else {
              workspaceTypes[1]++;
            }
          case BlazeWorkspace():
            workspaceTypes[3]++;
          case GnWorkspace():
            workspaceTypes[4]++;
          case BasicWorkspace():
            workspaceTypes[0]++;
        }
      }
      var fileSystemState = driver.fsState;
      // Capture the known files before the loop to prevent a concurrent
      // modification exception. The reason for the exception is unknown.
      var knownFiles = fileSystemState.knownFiles.toList();
      for (var fileState in knownFiles) {
        var isImmediate = fileState.path.startsWith(rootPath);
        if (isImmediate) {
          immediateFileCount++;
          immediateFileLineCount += fileState.lineInfo.lineCount;
          var libraryKind = fileState.kind.library;
          if (libraryKind != null) {
            libraryCycles.add(libraryKind.libraryCycle);
          }
        } else {
          var lineCount = fileState.lineInfo.lineCount;
          transitiveFileCount++;
          transitiveFileLineCount += lineCount;
          if (transitiveFilePaths.add(fileState.path)) {
            transitiveFileUniqueLineCount += lineCount;
          }
        }
      }
    }
    var transitiveFileUniqueCount = transitiveFilePaths.length;

    var libraryCycleLibraryCounts = <int>[];
    var libraryCycleLineCounts = <int>[];
    for (var libraryCycle in libraryCycles) {
      var libraries = libraryCycle.libraries;
      var lineCount = 0;
      for (var library in libraries) {
        for (var file in library.files) {
          lineCount += file.lineInfo.lineCount;
        }
      }
      libraryCycleLibraryCounts.add(libraries.length);
      libraryCycleLineCounts.add(lineCount);
    }

    analyticsManager.analysisComplete(
      numberOfContexts: driverMap.length,
      immediateFileCount: immediateFileCount,
      immediateFileLineCount: immediateFileLineCount,
      transitiveFileCount: transitiveFileCount,
      transitiveFileLineCount: transitiveFileLineCount,
      transitiveFileUniqueCount: transitiveFileUniqueCount,
      transitiveFileUniqueLineCount: transitiveFileUniqueLineCount,
      libraryCycleLibraryCounts: libraryCycleLibraryCounts,
      libraryCycleLineCounts: libraryCycleLineCounts,
      contextWorkspaceType: workspaceTypes,
      numberOfPackagesInWorkspace: numberOfPackages,
    );
  }

  Future<ResolvedForCompletionResultImpl?> resolveForCompletion({
    required String path,
    required int offset,
    required OperationPerformanceImpl performance,
  }) async {
    if (!file_paths.isDart(resourceProvider.pathContext, path)) {
      return null;
    }

    var driver = getAnalysisDriver(path);
    if (driver == null) {
      return null;
    }

    var result = _tryGetCachedResolvedUnitAsResolvedForCompletion(driver, path);
    if (result != null) return result;

    try {
      return await driver.resolveForCompletion(
        path: path,
        offset: offset,
        performance: performance,
      );
    } catch (e, st) {
      instrumentationService.logException(e, st);
    }
    return null;
  }

  Future<ResolvedForCompletionResultImpl?> resolveForCompletionWithLineColumn({
    required String path,
    required int line,
    required int column,
    required OperationPerformanceImpl performance,
  }) async {
    if (!file_paths.isDart(resourceProvider.pathContext, path)) {
      return null;
    }

    var driver = getAnalysisDriver(path);
    if (driver == null) {
      return null;
    }

    var result = _tryGetCachedResolvedUnitAsResolvedForCompletion(driver, path);
    if (result != null) return result;

    try {
      return await driver.resolveForCompletionWithLineColumn(
        path: path,
        line: line,
        column: column,
        performance: performance,
      );
    } catch (e, st) {
      instrumentationService.logException(e, st);
    }
    return null;
  }

  /// Sends an LSP notification to the client.
  ///
  /// The legacy server will wrap LSP notifications inside an
  /// 'lsp.notification' notification.
  void sendLspNotification(lsp.NotificationMessage notification);

  /// Sends an LSP request with the given [params] to the client and waits for a
  /// response. Completes with the raw [lsp.ResponseMessage] which could be an
  /// error response.
  Future<lsp.ResponseMessage> sendLspRequest(lsp.Method method, Object params);

  /// Sends an error notification to the user.
  void sendServerErrorNotification(
    String message,
    Object exception,
    StackTrace? stackTrace, {
    bool fatal = false,
  });

  /// Shows the user a prompt with some actions to select from using
  /// 'window/showMessageRequest'.
  ///
  /// Callers should use [userPromptSender] instead of calling this method
  /// directly because it returns null if the client does not support user
  /// prompts.
  @visibleForOverriding
  Future<String?> showUserPrompt(
    MessageType type,
    String message,
    List<String> actionLabels,
    lsp.CancellationToken cancellationToken,
  );

  @mustCallSuper
  Future<void> shutdown() async {
    await performanceLogger?.shutdown();
    completeLspUninitialization();

    await analysisDriverSchedulerEventsSubscription?.cancel();
    analysisDriverSchedulerEventsSubscription = null;

    // For now we record plugins only on shutdown. We might want to record them
    // every time the set of plugins changes, in which case we'll need to listen
    // to the `PluginManager.pluginsChanged` stream.
    await analyticsManager.changedPlugins(pluginManager);

    // For now we record context-dependent information only on shutdown. We
    // might want to record it on start-up as well.
    analyticsManager.createdAnalysisContexts(contextManager.analysisContexts);

    pubPackageService.shutdown();
    surveyManager?.shutdown();
    await contextManager.dispose();
    await analyticsManager.shutdown();
    await shutdownPerfWitness();
    await sessionLogger.shutdown();
  }

  ResolvedForCompletionResultImpl?
  _tryGetCachedResolvedUnitAsResolvedForCompletion(
    AnalysisDriver driver,
    String path,
  ) {
    try {
      ResolvedUnitResult? result = driver.getCachedResolvedUnit(path);
      if (result != null) {
        result as ResolvedUnitResultImpl;
        return ResolvedForCompletionResultImpl(
          analysisSession: result.session,
          fileState: result.fileState,
          path: path,
          uri: result.uri,
          exists: result.exists,
          content: result.content,
          lineInfo: result.lineInfo,
          parsedUnit: result.unit,
          libraryFragment: result.libraryFragment,
          resolvedNodes: [result.unit],
        );
      }
    } catch (e, st) {
      instrumentationService.logException(e, st);
    }
    return null;
  }
}

/// ContextManager callbacks that operate on the base server regardless
/// of protocol.
abstract class CommonServerContextManagerCallbacks
    extends ContextManagerCallbacks {
  /// The [ResourceProvider] by which paths are converted into [Resource]s.
  final OverlayResourceProvider resourceProvider;

  /// The set of files for which notifications were sent.
  final Set<String> filesToFlush = {};

  new(this.resourceProvider);

  @override
  @mustCallSuper
  void afterContextsCreated() {
    analysisServer.afterContextsCreated();
  }

  @override
  @mustCallSuper
  void afterContextsDestroyed() {
    flushResults(filesToFlush.toList());
    filesToFlush.clear();
    analysisServer.afterContextsDestroyed();
  }

  @override
  void afterWatchEvent(WatchEvent event) {}

  @override
  @mustCallSuper
  void applyFileRemoved(String file) {
    // If the removed file doesn't have an overlay, we need to clear any
    // previous results.
    if (!resourceProvider.hasOverlay(file)) {
      // Clear the cached errors in the notification manager so we don't
      // re-send stale results if a plugin sends an update and we merge it with
      // previous results.
      analysisServer.notificationManager.errors.clearResultsForFile(file);

      if (filesToFlush.remove(file)) {
        flushResults([file]);
      }
    }
  }

  @override
  @mustCallSuper
  void broadcastWatchEvent(WatchEvent event) {
    analysisServer.notifyFlutterWidgetDescriptions(event.path);
    analysisServer.pluginManager.broadcastWatchEvent(event);
  }

  void flushResults(List<String> files);

  @override
  @mustCallSuper
  void handleFileResult(FileResult result) {
    if (analysisServer.suppressAnalysisResults) {
      return;
    }

    var path = result.path;
    filesToFlush.add(path);

    if (result is AnalysisResultWithDiagnostics) {
      if (analysisServer.isAnalyzed(path)) {
        var serverErrors = server.doAnalysisError_listFromEngine(result);
        recordAnalysisErrors(path, serverErrors);
      }
    }

    if (result is ResolvedUnitResult) {
      analysisServer.filesResolvedSinceLastIdle.add(path);
      handleResolvedUnitResult(result);
    }
  }

  void handleResolvedUnitResult(ResolvedUnitResult result);

  @override
  @mustCallSuper
  void listenAnalysisDriver(analysis.AnalysisDriver driver) {
    driver.exceptions.listen(analysisServer.logExceptionResult);
    driver.priorityFiles = analysisServer.priorityFiles.toList();
  }

  @override
  void pubspecChanged(String path) {
    analysisServer.pubPackageService.fetchPackageVersionsViaPubOutdated(
      path,
      pubspecWasModified: true,
    );
  }

  @override
  void pubspecRemoved(String path) {
    analysisServer.pubPackageService.flushPackageCaches(path);
  }

  @override
  @mustCallSuper
  void recordAnalysisErrors(String path, List<server.AnalysisError> errors) {
    filesToFlush.add(path);
    analysisServer.notificationManager.recordAnalysisErrors(
      NotificationManager.serverId,
      path,
      errors,
    );
  }
}

/// A server-agnostic type for messages sent to the client.
enum MessageType {
  error(lsp.MessageType.Error, legacy.MessageType.ERROR),
  warning(lsp.MessageType.Warning, legacy.MessageType.WARNING),
  info(lsp.MessageType.Info, legacy.MessageType.INFO),
  log(lsp.MessageType.Log, legacy.MessageType.LOG);

  final lsp.MessageType forLsp;
  final legacy.MessageType forLegacy;

  new(this.forLsp, this.forLegacy);
}

class ServerRecentPerformance {
  /// The maximum number of performance measurements to keep.
  static const int performanceListMaxLength = 250;

  /// The maximum number of slow performance measurements to keep.
  static const int slowRequestsListMaxLength = 1000;

  /// The duration we use to categorize requests as slow.
  static const Duration slowRequestsThreshold = Duration(milliseconds: 500);

  /// A list of code completion performance measurements for the latest
  /// completion operation up to [performanceListMaxLength] measurements.
  final RecentBuffer<CompletionPerformance> completion =
      RecentBuffer<CompletionPerformance>(performanceListMaxLength);

  /// A list of performance measurements for the latest requests to get fixes
  /// up to [performanceListMaxLength] measurements.
  final RecentBuffer<GetAssistsPerformance> getAssists =
      RecentBuffer<GetAssistsPerformance>(performanceListMaxLength);

  /// A list of performance measurements for the latest requests to get fixes
  /// up to [performanceListMaxLength] measurements.
  final RecentBuffer<GetFixesPerformance> getFixes =
      RecentBuffer<GetFixesPerformance>(performanceListMaxLength);

  /// A list of performance measurements for the latest requests to get refactorings
  /// up to [performanceListMaxLength] measurements.
  final RecentBuffer<GetRefactoringsPerformance> getRefactorings =
      RecentBuffer<GetRefactoringsPerformance>(performanceListMaxLength);

  /// A [RecentBuffer] for performance information about the most recent
  /// requests.
  final RecentBuffer<RequestPerformance> requests = RecentBuffer(
    performanceListMaxLength,
  );

  /// A [RecentBuffer] for performance information about the most recent
  /// slow requests.
  final RecentBuffer<RequestPerformance> slowRequests = RecentBuffer(
    slowRequestsListMaxLength,
  );
}

extension on OverlayResourceProvider {
  /// The path to the location of the byte store on disk, or `null` if there is
  /// no on-disk byte store.
  String? get byteStorePath {
    if (baseProvider is PhysicalResourceProvider) {
      var stateLocation = baseProvider.getStateLocation('.analysis-driver');
      return stateLocation?.path;
    }
    return null;
  }
}
