// Copyright (c) 2019, 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 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/source.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/file_system/file_system.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/lint/pub.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/summary/api_signature.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/uri.dart';
import 'package:analyzer/src/utilities/uri_cache.dart';
import 'package:analyzer/src/workspace/basic.dart';
import 'package:analyzer/src/workspace/simple.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart';
import 'package:pub_semver/pub_semver.dart';

/// Return the content of the [file], `null` if cannot be read.
String? _fileContentOrNull(File file) {
  try {
    return file.readAsStringSync();
  } catch (_) {
    return null;
  }
}

/// Check if the given list of path components contains a package build
/// generated directory, it would have the following path segments,
/// '.dart_tool/build/generated'.
bool _isPackageBuildGeneratedPath(List<String> pathComponents, int startIndex) {
  if (pathComponents.length > startIndex + 2) {
    if (pathComponents[startIndex] == file_paths.dotDartTool &&
        pathComponents[startIndex + 1] == file_paths.packageBuild &&
        pathComponents[startIndex + 2] == file_paths.packageBuildGenerated) {
      return true;
    }
  }
  return false;
}

/// Instances of the class `PackageBuildFileUriResolver` resolve `file` URI's by
/// first resolving file uri's in the expected way, and then by looking in the
/// corresponding generated directories.
class PackageConfigFileUriResolver extends ResourceUriResolver {
  final PackageConfigWorkspace workspace;

  PackageConfigFileUriResolver(this.workspace) : super(workspace.provider);

  @override
  Uri pathToUri(String path) {
    var pathContext = workspace.provider.pathContext;

    if (pathContext.isWithin(workspace.root, path)) {
      var package = workspace.findPackageFor(path);
      if (package is PubPackage) {
        var relative = pathContext.relative(path, from: workspace.root);
        var components = pathContext.split(relative);
        if (components.length > 4 &&
            _isPackageBuildGeneratedPath(components, 0) &&
            components[3] == package._name) {
          var canonicalPath = pathContext.joinAll([
            workspace.root,
            ...components.skip(4),
          ]);
          return pathContext.toUri(canonicalPath);
        }
      }
    }

    return super.pathToUri(path);
  }

  @override
  Source? resolveAbsolute(Uri uri) {
    if (!ResourceUriResolver.isFileUri(uri)) {
      return null;
    }
    String filePath = fileUriToNormalizedPath(provider.pathContext, uri);
    Resource resource = provider.getResource(filePath);
    if (resource is! File) {
      return null;
    }
    var file = workspace.findFile(filePath);
    if (file != null) {
      return FileSource(file, uri);
    }
    return null;
  }
}

/// The [UriResolver] that can resolve `package` URIs in a
/// [PackageConfigWorkspace].
class PackageConfigPackageUriResolver extends UriResolver {
  final PackageConfigWorkspace _workspace;
  final UriResolver _normalUriResolver;
  final Context _context;

  PackageConfigPackageUriResolver(
    PackageConfigWorkspace workspace,
    this._normalUriResolver,
  ) : _workspace = workspace,
      _context = workspace.provider.pathContext;

  // TODO(scheglov): Finish switching to [Packages].
  Map<String, List<Folder>> get packageMap => _workspace.packageMap;

  @override
  Uri? pathToUri(String path) {
    if (_context.isWithin(_workspace.root, path)) {
      var uriParts = _restoreUriParts(path);
      if (uriParts != null) {
        return uriCache.parse('package:${uriParts[0]}/${uriParts[1]}');
      }
    }
    return _normalUriResolver.pathToUri(path);
  }

  @override
  Source? resolveAbsolute(Uri uri) {
    if (!uri.isScheme('package')) {
      return null;
    }

    var basicResolverSource = _normalUriResolver.resolveAbsolute(uri);
    if (basicResolverSource != null && basicResolverSource.exists()) {
      return basicResolverSource;
    }

    String uriPath = uri.path;
    int slash = uriPath.indexOf('/');

    // If the path either starts with a slash or has no slash, it is invalid.
    if (slash < 1) {
      return null;
    }

    String packageName = uriPath.substring(0, slash);
    String fileUriPart = uriPath.substring(slash + 1);
    String filePath = fileUriPart.replaceAll('/', _context.separator);

    var file = _workspace.builtFile(
      _workspace._builtPackageSourcePath(filePath),
      packageName,
    );
    if (file != null && file.exists) {
      return FileSource(file, uri);
    }
    return basicResolverSource;
  }

  List<String>? _restoreUriParts(String filePath) {
    String relative = _context.relative(filePath, from: _workspace.root);
    List<String> components = _context.split(relative);
    if (components.length > 5 &&
        components[0] == '.dart_tool' &&
        components[1] == 'build' &&
        components[2] == 'generated' &&
        components[4] == 'lib') {
      String packageName = components[3];
      String pathInLib = components.skip(5).join('/');
      return [packageName, pathInLib];
    }
    return null;
  }
}

/// Information about a Package Config workspace.
class PackageConfigWorkspace extends SimpleWorkspace {
  /// The associated package config file.
  final File packageConfigFile;

  /// The contents of the package config file.
  late final String? _packageConfigContent;

  /// Indicates whether this is a PubWorkspace as per
  /// https://dart.dev/tools/pub/workspaces
  bool _isPubWorkspace = false;

  final Map<String, WorkspacePackageImpl> _workspacePackages = {};

  factory PackageConfigWorkspace(
    ResourceProvider provider, //Packages packages,
    String root,
    File packageConfigFile,
    Packages packages,
  ) {
    // Use the default packages if there is one.
    var newPackages = packages == Packages.empty
        ? parsePackageConfigJsonFile(provider, packageConfigFile)
        : packages;

    return PackageConfigWorkspace._(
      provider,
      newPackages,
      root,
      packageConfigFile,
    );
  }

  PackageConfigWorkspace._(
    super.provider,
    super.packages,
    super.root,
    this.packageConfigFile,
  ) {
    _packageConfigContent = packageConfigFile.readAsStringSync();
    var pubspecFile = provider
        .getFolder(root)
        .getChildAssumingFile(file_paths.pubspecYaml);
    if (pubspecFile.exists) {
      var pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
      if (pubspec.workspace != null) {
        _isPubWorkspace = true;
      }
    }
  }

  Iterable<WorkspacePackageImpl> get allPackages =>
      _workspacePackages.values.toSet();

  @override
  bool get isConsistentWithFileSystem {
    return _fileContentOrNull(packageConfigFile) == _packageConfigContent;
  }

  bool get isPubWorkspace => _isPubWorkspace;

  @override
  UriResolver get packageUriResolver {
    return PackageConfigPackageUriResolver(
      this,
      PackageMapUriResolver(provider, packageMap),
    );
  }

  /// For some package file, which may or may not be a package source (it could
  /// be in `bin/`, `web/`, etc), find where its built counterpart will exist if
  /// it's a generated source.
  ///
  /// To get a [builtPath] for a package source file to use in this method,
  /// use [_builtPackageSourcePath]. For `bin/`, `web/`, etc, it must be relative
  /// to the project root.
  @visibleForTesting
  File? builtFile(String builtPath, String packageName) {
    if (packages[packageName] == null) {
      return null;
    }
    var context = provider.pathContext;
    var fullBuiltPath = context.normalize(
      context.join(
        root,
        file_paths.dotDartTool,
        file_paths.packageBuild,
        file_paths.packageBuildGenerated,
        packageName,
        builtPath,
      ),
    );
    return provider.getFile(fullBuiltPath);
  }

  @internal
  @override
  void contributeToResolutionSalt(ApiSignature buffer) {
    buffer.addString(_packageConfigContent ?? '');
  }

  @override
  SourceFactory createSourceFactory(
    DartSdk? sdk,
    SummaryDataStore? summaryData,
  ) {
    var resolvers = <UriResolver>[];
    if (sdk != null) {
      resolvers.add(DartUriResolver(sdk));
    }
    if (summaryData != null) {
      resolvers.add(InSummaryUriResolver(summaryData));
    }
    resolvers.add(packageUriResolver);
    resolvers.add(PackageConfigFileUriResolver(this));
    resolvers.add(ResourceUriResolver(provider));
    return SourceFactory(resolvers);
  }

  /// Return the file with the given [filePath], looking first in the generated
  /// directory `.dart_tool/build/generated/$projectPackageName/`, then in
  /// source directories.
  ///
  /// The file in the workspace [root] is returned even if it does not exist.
  /// Return `null` if the given [filePath] is not in the workspace root.
  File? findFile(String filePath) {
    var context = provider.pathContext;
    assert(context.isAbsolute(filePath), 'Not an absolute path: $filePath');
    try {
      var package = findPackageFor(filePath);
      if (package is PubPackage) {
        var relativePath = context.relative(filePath, from: package.root.path);
        var file = builtFile(relativePath, package._name ?? '');
        if (file!.exists) {
          return file;
        }
      }
      return provider.getFile(filePath);
    } catch (_) {
      return null;
    }
  }

  /// Find the [PubPackage] that contains the given file path. The path
  /// can be for a source file or a generated file. Generated files are located
  /// in the '.dart_tool/build/generated' folder of the containing package.
  @override
  WorkspacePackageImpl? findPackageFor(String filePath) {
    var pathContext = provider.pathContext;
    // Must be in this workspace.
    if (!pathContext.isWithin(root, filePath)) {
      return null;
    }

    List<String> paths = [];
    var folder = provider.getFile(filePath).parent;

    for (var current in folder.withAncestors) {
      var package = _workspacePackages[current.path];
      if (package != null) {
        for (var path in paths) {
          _workspacePackages[path] = package;
        }
        return package;
      }
      var pubspec = current.getChildAssumingFile(file_paths.pubspecYaml);
      if (pubspec.exists) {
        if (_isInThirdPartyDart(pubspec)) {
          return null;
        }
        var package = PubPackage(current, this, pubspec);
        for (var path in paths) {
          _workspacePackages[path] = package;
        }
        _workspacePackages[current.path] = package;
        return package;
      }
      if (current.path == root) {
        var package = BasicWorkspacePackage(current, this);
        for (var path in paths) {
          _workspacePackages[path] = package;
        }
        return package;
      }
      paths.add(current.path);
    }
    return null;
  }

  /// Unlike the way that sources are resolved against `.packages` (if foo
  /// points to folder bar, then `foo:baz.dart` is found at `bar/baz.dart`), the
  /// built sources for a package require the `lib/` prefix first. This is
  /// because `bin/`, `web/`, and `test/` etc can all be built as well. This
  /// method exists to give a name to that prefix processing step.
  String _builtPackageSourcePath(String filePath) {
    var context = provider.pathContext;
    assert(context.isRelative(filePath), 'Not a relative path: $filePath');
    return context.join('lib', filePath);
  }

  /// Find the package config workspace that contains the given [filePath].
  /// A [PackageConfigWorkspace] is rooted at the innermost package-config file.
  static PackageConfigWorkspace? find(
    ResourceProvider provider,
    Packages packages,
    String filePath,
  ) {
    var start = provider.getFolder(filePath);
    for (var current in start.withAncestors) {
      var packageConfigFile = current
          .getChildAssumingFolder(file_paths.dotDartTool)
          .getChildAssumingFile(file_paths.packageConfigJson);
      if (packageConfigFile.exists) {
        var root = current.path;
        return PackageConfigWorkspace(
          provider,
          root,
          packageConfigFile,
          packages,
        );
      }
    }
    return null;
  }

  /// See https://buganizer.corp.google.com/issues/273584249
  ///
  /// Check if `/home/workspace/third_party/dart/my/pubspec.yaml`
  /// If so, we are in a Blaze workspace, and should not create Pub.
  static bool _isInThirdPartyDart(File pubspec) {
    var path = pubspec.path;
    var pathContext = pubspec.provider.pathContext;
    var pathComponents = pathContext.split(path);
    return pathComponents.length > 4 &&
        pathComponents[pathComponents.length - 3] == 'dart' &&
        pathComponents[pathComponents.length - 4] == 'third_party';
  }
}

/// Information about a package defined in a [PackageConfigWorkspace].
///
/// Separate from [Packages] or package maps, this class is designed to simply
/// understand whether arbitrary file paths represent libraries declared within
/// a given package in a [PackageConfigWorkspace].
class PubPackage extends WorkspacePackageImpl {
  static const List<String> _generatedPathParts = [
    file_paths.dotDartTool,
    file_paths.packageBuild,
    file_paths.packageBuildGenerated,
  ];

  @override
  final Folder root;

  final String? _name;

  final String? pubspecContent;

  final Pubspec? pubspec;

  final File pubspecFile;

  VersionConstraint? _sdkVersionConstraint;

  /// A flag to indicate if we've tried to parse the sdk constraint.
  bool _parsedSdkConstraint = false;

  @override
  final PackageConfigWorkspace workspace;

  factory PubPackage(
    Folder root,
    PackageConfigWorkspace workspace,
    File pubspecFile,
  ) {
    var pubspecContent = pubspecFile.readAsStringSync();
    var pubspec = Pubspec.parse(pubspecContent);
    var packageName = pubspec.name?.value.text;
    return PubPackage._(
      root,
      workspace,
      pubspecContent,
      pubspecFile,
      pubspec,
      packageName,
    );
  }

  PubPackage._(
    this.root,
    this.workspace,
    this.pubspecContent,
    this.pubspecFile,
    this.pubspec,
    this._name,
  );

  /// The version range for the SDK specified for this package , or `null` if
  /// it is ill-formatted or not set.
  VersionConstraint? get sdkVersionConstraint {
    if (!_parsedSdkConstraint) {
      _parsedSdkConstraint = true;

      var sdkValue = pubspec?.environment?.sdk?.value.text;
      if (sdkValue != null) {
        try {
          _sdkVersionConstraint = VersionConstraint.parse(sdkValue);
        } catch (_) {
          // Ill-formatted constraints, default to a `null` value.
        }
      }
    }
    return _sdkVersionConstraint;
  }

  @override
  bool contains(Source source) {
    var uri = source.uri;

    if (uri.isScheme('package')) {
      // TODO(keertip): Check to see if we can use information from package
      // config to find out if a file is in this package.
      var packageName = uri.pathSegments[0];
      return packageName == _name;
    }

    if (uri.isScheme('file')) {
      var path = source.fullName;
      return workspace.findPackageFor(path) != null;
    }
    return false;
  }

  @override
  bool isInTestDirectory(File file) {
    return root.getChildAssumingFolder('test').contains(file.path);
  }

  @override
  Packages packagesAvailableTo(String libraryPath) {
    // TODO(brianwilkerson): Consider differentiating based on whether the
    //  [libraryPath] is inside the `lib` directory.
    return workspace.packages;
  }

  @override
  /// A Pub package's public API consists of libraries found in the top-level
  /// "lib" directory, and any subdirectories, excluding the "src" directory
  /// just inside the top-level "lib" directory.
  bool sourceIsInPublicApi(Source source) {
    var filePath = filePathFromSource(source);
    if (filePath == null) return false;
    var libFolder = root.getChildAssumingFolder('lib');
    if (libFolder.contains(filePath)) {
      // A file in "$root/lib" is public iff it is not in "$root/lib/src".
      var libSrcFolder = libFolder.getChildAssumingFolder('src');
      return !libSrcFolder.contains(filePath);
    }

    Folder intermediateFolder = root;
    for (String part in _generatedPathParts) {
      intermediateFolder = intermediateFolder.getChildAssumingFolder(part);
    }
    libFolder = intermediateFolder
        .getChildAssumingFolder('test')
        .getChildAssumingFolder('lib');

    if (libFolder.contains(filePath)) {
      // A file in "$generated/lib" is public iff it is not in
      // "$generated/lib/src".
      var libSrcFolder = libFolder.getChildAssumingFolder('src');
      return !libSrcFolder.contains(filePath);
    }
    return false;
  }
}
