// Copyright (c) 2020, 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:collection/collection.dart';
import 'package:file/file.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart';

/// A reader for Dart sources and related source maps.
abstract class AssetReader {
  /// Base path of the application, for example, set up in the index file:
  ///
  /// ```
  /// <html>
  ///   <head>
  ///     <base href="/abc/">
  ///     <script src="main.dart.js"></script>
  ///   </head>
  /// </html>
  /// ```
  String get basePath;

  /// Returns the contents for a source map at the provided server path, or
  /// null if the resource does not exist.
  Future<String?> sourceMapContents(String serverPath);

  /// Returns the contents for a dart source at the provided server path, or
  /// null if the resource does not exist.
  Future<String?> dartSourceContents(String serverPath);

  /// Returns the contents for the merged metadata output at the provided path,
  /// or null if the resource does not exist.
  Future<String?> metadataContents(String serverPath);

  /// Closes connections
  Future<void> close();
}

class PackageUriMapper {
  final _logger = Logger('PackageUriMapper');
  final PackageConfig packageConfig;
  final bool useDebuggerModuleNames;

  static Future<PackageUriMapper> create(
    FileSystem fileSystem,
    Uri packageConfigFile, {
    bool useDebuggerModuleNames = false,
  }) async {
    final packageConfig =
        await loadPackageConfig(fileSystem.file(packageConfigFile));
    return PackageUriMapper(
      packageConfig,
      useDebuggerModuleNames: useDebuggerModuleNames,
    );
  }

  PackageUriMapper(this.packageConfig, {this.useDebuggerModuleNames = false});

  /// Compute server path for package uri.
  ///
  /// Note: needs to match `urlForComponentUri` in javascript_bundle.dart
  /// in SDK code.
  String? packageUriToServerPath(Uri packageUri) {
    final defaultServerPath = '/packages/${packageUri.path}';
    if (packageUri.isScheme('package')) {
      if (!useDebuggerModuleNames) {
        return defaultServerPath;
      }
      final resolvedUri = packageConfig.resolve(packageUri);
      if (resolvedUri == null) {
        _logger.severe('Cannot resolve package uri $packageUri');
        return defaultServerPath;
      }
      final package = packageConfig.packageOf(resolvedUri);
      if (package == null) {
        _logger.severe('Cannot find package for package uri $packageUri');
        return defaultServerPath;
      }
      final root = package.root;
      final relativeUrl = resolvedUri.toString().replaceFirst('$root', '');
      final relativeRoot = _getRelativeRoot(root);
      final ret = relativeRoot == null
          ? 'packages/$relativeUrl'
          : 'packages/$relativeRoot/$relativeUrl';
      return ret;
    }
    _logger.severe('Expected package uri, but found $packageUri');
    return null;
  }

  /// Compute resolved file uri for a server path.
  Uri? serverPathToResolvedUri(String serverPath) {
    serverPath = stripLeadingSlashes(serverPath);
    final segments = serverPath.split('/');
    if (segments.first == 'packages') {
      if (!useDebuggerModuleNames) {
        return packageConfig
            .resolve(Uri(scheme: 'package', pathSegments: segments.skip(1)));
      }
      final relativeRoot = segments.skip(1).first;
      final relativeUrl = segments.skip(2).join('/');
      final package = packageConfig.packages
          .firstWhere((Package p) => _getRelativeRoot(p.root) == relativeRoot);
      final resolvedUri = package.root.resolve(relativeUrl);

      return resolvedUri;
    }
    _logger.severe('Expected "packages/" path, but found $serverPath');
    return null;
  }
}

String stripLeadingSlashes(String path) {
  while (path.startsWith('/')) {
    path = path.substring(1);
  }
  return path;
}

String? _getRelativeRoot(Uri root) =>
    root.pathSegments.lastWhereOrNull((segment) => segment.isNotEmpty);
