// Copyright (c) 2018, 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:analyzer/dart/analysis/declared_variables.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
import 'package:analyzer/src/context/builder.dart' show locateEmbedderYamlFor;
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/dart/analysis/analysis_options.dart';
import 'package:analyzer/src/dart/analysis/analysis_options_map.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart'
    show ByteStore, MemoryByteStore;
import 'package:analyzer/src/dart/analysis/context_root.dart';
import 'package:analyzer/src/dart/analysis/driver.dart'
    show
        AnalysisDriver,
        AnalysisDriverScheduler,
        AnalysisDriverTestView,
        OwnedFiles;
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart'
    show PerformanceLog;
import 'package:analyzer/src/dart/analysis/unlinked_unit_store.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/sdk.dart' show DartSdk;
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/summary/summary_sdk.dart';
import 'package:analyzer/src/summary2/package_bundle_format.dart';
import 'package:analyzer/src/workspace/workspace.dart';

/// A utility class used to build an analysis context based on a context root.
class ContextBuilderImpl {
  /// The resource provider used to access the file system.
  final ResourceProvider resourceProvider;

  /// Analysis options mappings shared by all contexts built by this builder.
  final AnalysisOptionsMap _optionsMap = AnalysisOptionsMap();

  /// Initialize a newly created context builder. If a [resourceProvider] is
  /// given, then it will be used to access the file system, otherwise the
  /// default resource provider will be used.
  ContextBuilderImpl({ResourceProvider? resourceProvider})
    : resourceProvider = resourceProvider ?? PhysicalResourceProvider.INSTANCE;

  /// Return an analysis context corresponding to the given [contextRoot].
  ///
  /// If a set of [declaredVariables] is provided, the values will be used to
  /// map the variable names found in `fromEnvironment` invocations to the
  /// constant value that will be returned. If none is given, then no variables
  /// will be defined.
  ///
  /// If a list of [librarySummaryPaths] is provided, then the summary files at
  /// those paths will be used, when possible, when analyzing the libraries
  /// contained in the summary files.
  ///
  /// If an [sdkPath] is provided, and if it is a valid path to a directory
  /// containing a valid SDK, then the SDK in the referenced directory will be
  /// used when analyzing the code in the context.
  ///
  /// If an [sdkSummaryPath] is provided, then that file will be used as the
  /// summary file for the SDK.
  DriverBasedAnalysisContext createContext({
    ByteStore? byteStore,
    required ContextRootImpl contextRoot,
    bool definedOptionsFile = false,
    DeclaredVariables? declaredVariables,
    bool drainStreams = true,
    bool enableIndex = false,
    List<String>? librarySummaryPaths,
    PerformanceLog? performanceLog,
    bool retainDataForTesting = false,
    AnalysisDriverScheduler? scheduler,
    required String sdkPath,
    String? sdkSummaryPath,
    void Function({
      required AnalysisOptionsImpl analysisOptions,
      required DartSdk sdk,
    })?
    updateAnalysisOptions3,
    FileContentCache? fileContentCache,
    UnlinkedUnitStore? unlinkedUnitStore,
    OwnedFiles? ownedFiles,
    bool enableLintRuleTiming = false,
    required bool withFineDependencies,
    List<String> enabledExperiments = const [],
  }) {
    byteStore ??= MemoryByteStore();
    performanceLog ??= PerformanceLog(null);

    if (scheduler == null) {
      scheduler = AnalysisDriverScheduler(performanceLog);
      scheduler.start();
    }

    SummaryDataStore? summaryData;
    if (librarySummaryPaths != null) {
      summaryData = SummaryDataStore();
      for (var summaryPath in librarySummaryPaths) {
        var bytes = resourceProvider.getFile(summaryPath).readAsBytesSync();
        var bundle = PackageBundleReader(bytes);
        summaryData.addBundle(summaryPath, bundle);
      }
    }

    var workspace = contextRoot.workspace;
    var sdk = _createSdk(
      workspace: workspace,
      sdkPath: sdkPath,
      sdkSummaryPath: sdkSummaryPath,
    );

    // TODO(scheglov): Ensure that "librarySummaryPaths" not null only
    // when "sdkSummaryPath" is not null.
    if (sdk is SummaryBasedDartSdk) {
      summaryData?.addBundle(null, sdk.bundle);
    }

    var optionsFile = contextRoot.optionsFile;
    var sourceFactory = workspace.createSourceFactory(sdk, summaryData);

    AnalysisOptionsMap analysisOptionsMap;
    // If there's an options file defined (as, e.g. passed into the
    // AnalysisContextCollection), use a shared options map based on it.
    if (definedOptionsFile && optionsFile != null) {
      analysisOptionsMap = AnalysisOptionsMap.forSharedOptions(
        _getAnalysisOptions(
          optionsFile,
          sourceFactory,
          sdk,
          updateAnalysisOptions3,
          enabledExperiments,
        ),
      );
    } else {
      // Otherwise, create one from the options file mappings stored in the
      // context root.
      analysisOptionsMap = _createOptionsMap(
        contextRoot,
        sourceFactory,
        updateAnalysisOptions3,
        sdk,
      );
    }

    var analysisContext = DriverBasedAnalysisContext(
      resourceProvider,
      contextRoot,
    );
    var driver = AnalysisDriver(
      scheduler: scheduler,
      logger: performanceLog,
      resourceProvider: resourceProvider,
      byteStore: byteStore,
      sourceFactory: sourceFactory,
      analysisOptionsMap: analysisOptionsMap,
      packages: _createPackageMap(contextRoot: contextRoot),
      analysisContext: analysisContext,
      enableIndex: enableIndex,
      externalSummaries: summaryData,
      retainDataForTesting: retainDataForTesting,
      fileContentCache: fileContentCache,
      unlinkedUnitStore: unlinkedUnitStore,
      declaredVariables: declaredVariables,
      testView: retainDataForTesting ? AnalysisDriverTestView() : null,
      ownedFiles: ownedFiles,
      enableLintRuleTiming: enableLintRuleTiming,
      withFineDependencies: withFineDependencies,
    );

    // AnalysisDriver reports results into streams.
    // We need to drain these streams to avoid memory leak.
    if (drainStreams) {
      unawaited(driver.exceptions.drain<void>());
    }

    return analysisContext;
  }

  /// Create an [AnalysisOptionsMap] for the given [contextRoot].
  AnalysisOptionsMap _createOptionsMap(
    ContextRootImpl contextRoot,
    SourceFactory sourceFactory,
    void Function({
      required AnalysisOptionsImpl analysisOptions,
      required DartSdk sdk,
    })?
    updateAnalysisOptions,
    DartSdk sdk,
  ) {
    var provider = AnalysisOptionsProvider(sourceFactory);

    var optionsMappings = contextRoot.optionsFileMap.entries;
    for (var MapEntry(key: file, value: folders) in optionsMappings) {
      var options = AnalysisOptionsImpl.fromYaml(
        optionsMap: provider.getOptionsFromFile(file),
        file: file,
        resourceProvider: resourceProvider,
      );

      for (var folder in folders) {
        _optionsMap[folder] = options;
      }
    }

    for (var options in _optionsMap.options) {
      if (updateAnalysisOptions != null) {
        updateAnalysisOptions(analysisOptions: options, sdk: sdk);
      }
    }
    return _optionsMap;
  }

  /// Return [Packages] to analyze the [contextRoot].
  ///
  // TODO(scheglov): Get [Packages] from [Workspace]?
  Packages _createPackageMap({required ContextRootImpl contextRoot}) {
    var packagesFile = contextRoot.packagesFile;
    if (packagesFile != null) {
      return parsePackageConfigJsonFile(resourceProvider, packagesFile);
    } else {
      return Packages.empty;
    }
  }

  /// Return the SDK that should be used to analyze code.
  DartSdk _createSdk({
    required Workspace workspace,
    String? sdkPath,
    String? sdkSummaryPath,
  }) {
    if (sdkSummaryPath != null) {
      var file = resourceProvider.getFile(sdkSummaryPath);
      var bytes = file.readAsBytesSync();
      return SummaryBasedDartSdk.forBundle(PackageBundleReader(bytes));
    }

    var folderSdk = FolderBasedDartSdk(
      resourceProvider,
      resourceProvider.getFolder(sdkPath!),
    );

    {
      // TODO(scheglov): We already had partial SourceFactory in ContextLocatorImpl.
      var partialSourceFactory = workspace.createSourceFactory(null, null);
      var embedderYamlSource = partialSourceFactory.forUri(
        'package:sky_engine/_embedder.yaml',
      );
      if (embedderYamlSource != null) {
        var embedderYamlPath = embedderYamlSource.fullName;
        var libFolder = resourceProvider.getFile(embedderYamlPath).parent;
        var embedderYaml = locateEmbedderYamlFor(libFolder);
        if (embedderYaml != null) {
          return EmbedderSdk.new2(
            resourceProvider,
            libFolder,
            embedderYaml,
            languageVersion: folderSdk.languageVersion,
          );
        }
      }
    }

    return folderSdk;
  }

  /// Return the analysis options from [optionsFile].
  ///
  // TODO(scheglov): We have already loaded it once in [ContextLocatorImpl].
  AnalysisOptionsImpl _getAnalysisOptions(
    File optionsFile,
    SourceFactory sourceFactory,
    DartSdk sdk,
    void Function({
      required AnalysisOptionsImpl analysisOptions,
      required DartSdk sdk,
    })?
    updateAnalysisOptions,
    List<String> enabledExperiments,
  ) {
    AnalysisOptionsImpl options;

    try {
      var provider = AnalysisOptionsProvider(sourceFactory);
      options = AnalysisOptionsImpl.fromYaml(
        optionsMap: provider.getOptionsFromFile(optionsFile),
        file: optionsFile,
        resourceProvider: resourceProvider,
      );
    } catch (e) {
      // Ignore exception.
      options = AnalysisOptionsImpl(file: optionsFile);
    }

    options.contextFeatures = FeatureSet.fromEnableFlags2(
      sdkLanguageVersion: sdk.languageVersion,
      flags: enabledExperiments,
    );

    if (updateAnalysisOptions != null) {
      updateAnalysisOptions(analysisOptions: options, sdk: sdk);
    }

    return options;
  }
}
