// Copyright (c) 2017, 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.

/// Library specification in-memory representation.
///
/// Many dart tools are configurable to support different target platforms.  For
/// a given target, they need to know what libraries are available and where are
/// the sources and target-specific patches.
///
/// Here we define APIs to represent this specification and implement
/// serialization to (and deserialization from) a JSON file.
///
/// Here is an example specification JSON file:
///
///     {
///       "vm": {
///         "libraries": {
///             "core": {
///                "uri": "async/core.dart",
///                "patches": [
///                    "path/to/core_patch.dart",
///                    "path/to/list_patch.dart"
///                ]
///             }
///             "async": {
///                "uri": "async/async.dart",
///                "patches": "path/to/async_patch.dart"
///             }
///             "convert": {
///                "uri": "convert/convert.dart",
///             }
///             "mirrors": {
///                "uri": "mirrors/mirrors.dart",
///                "support_conditional_import": false
///             }
///         }
///       }
///     }
///
/// The format contains:
///   - a top level entry for each target. Keys are target names (e.g. "vm"
///     above), and values contain the entire specification of a target.
///
///   - each target specification is a map. The supported keys are "libraries"
///     and "include".
///
///   - The "libraries" entry contains details for how each platform library is
///     implemented. The entry is a map, where keys are the name of the platform
///     library and values contain details for where to find the implementation
///     fo that library.
///
///   - The name of the library is a single token (e.g. "core") that matches the
///     Uri path used after `dart:` (e.g. "dart:core").
///
///   - The "uri" entry on the library information is mandatory. The value is a
///     string URI reference. The "patches" entry is optional and may have as a
///     value a string URI reference or a list of URI references.
///
///     All URI references can either be a file URI or a relative URI path,
///     which will be resolved relative to the location of the library
///     specification file.
///
///   - The "support_conditional_import" entry on the library information is
///     optional. The value is a boolean indicating whether the library is
///     supported in the underlying target.  However, since the libraries are
///     assumed to be supported by default, we only expect users to use
///     `false`.
///
///     The purpose of this value is to configure conditional imports and
///     environment constants. By default every platform library that is
///     available in the "libraries" section implicitly defines an environment
///     variable `dart.library.name` as `"true"`, to indicate that the library
///     is supported.  Some backends allow imports to an unsupported platform
///     library (turning a static error into a runtime error when the library
///     is eventually accessed). These backends can use
///     `support_conditional_import: false` to report that such library is
///     still not supported in conditional imports and const `fromEnvironment`
///     expressions.
///
///     Internal libraries are never supported through conditional imports and
///     const `fromEnvironment` expressions.
///
///   - The "support_direct_import" entry on the library information is
///     optional. The value is an enumerated type indicating whether the library
///     is allowed to be imported by the underlying target. This property can
///     take one of the following values:
///
///       - "always": the library is supported by the underlying target and
///          can always be imported. This is the default.
///       - "never": the library is not supported by the underlying target and
///          imports of the library will be treated as compile time errors.
///       - "with_flag": the library is only supported when the
///         `--include-unsupported-platform-library-stubs` flag is set. If the
///          flag is not set, imports of the library will be treated as
///          compile time errors. Otherwise, the import will be allowed.
///
///     Some backends allow imports to an unsupported platform library (turning
///     a static error into a runtime error when the library is eventually
///     accessed). These backends can use `support_direct_import: "with_flag"`
///     to control access to these libraries, allowing for developer tooling to
///     handle code which imports unsupported libraries for a given platform.
///
///   - The "include" entry is a list of maps, each containing either a "path"
///     and a "target" entry, or only a "target" entry.
///
///     If both "path" and "target" entries are present, the libraries
///     specification file located at "path", relative to the location current
///     libraries specification file is loaded, and the libraries defined
///     in the target with the target name of the "target" entry are included in
///     this target.
///
///     If only the "target" is present, the libraries in the target in the
///     current libraries specification file with the target name of the
///     "target" entry are included in this target.
///
///     The "include" mechanism support transitive inclusion but doesn't allow
///     cyclic dependencies.
///
///     If the same library is defined in multiple included target
///     specifications, the last included takes precedence. This means that
///     if a target specification include a library and also defines it itself,
///     the latter is used.
///
///     Currently it is not supported to include a subset of the libraries from
///     an included target specifications.
///
///
/// Note: we currently have several different files that need to be updated
/// when changing libraries, sources, and patch files:
///    * .platform files (for dart2js)
///    * .gypi files (for vm)
///    * sdk_library_metadata/lib/libraries.dart (for analyzer, ddc)
///
/// we are in the process of unifying them all under this format (see
/// https://github.com/dart-lang/sdk/issues/28836), but for now we need to pay
/// close attention to change them consistently.

import 'dart:convert' show jsonDecode, jsonEncode;

import 'relativize.dart' show relativizeUri, isWindows;

/// Contents from a single library specification file.
///
/// Contains information about all libraries on all target platforms defined in
/// that file.
class LibrariesSpecification {
  final Uri specUri;
  final Map<String, TargetLibrariesSpecification> _targets;

  const LibrariesSpecification(
    this.specUri, [
    this._targets = const <String, TargetLibrariesSpecification>{},
  ]);

  /// The library specification for a given [target], or throws if none is
  /// available.
  TargetLibrariesSpecification specificationFor(String target) {
    TargetLibrariesSpecification? targetSpec = _targets[target];
    if (targetSpec == null) {
      throw new LibrariesSpecificationException(
        messageMissingTarget(target, specUri),
      );
    }
    return targetSpec;
  }

  static Future<LibrariesSpecification> load(
    Uri uri,
    Future<String> Function(Uri uri) read,
  ) {
    Map<Uri, LibrariesSpecification?> cache = {};
    Future<LibrariesSpecification> loadSpecification(Uri uri) async {
      if (cache.containsKey(uri)) {
        LibrariesSpecification? specification = cache[uri];
        if (specification == null) {
          throw new LibrariesSpecificationException(messageCyclicSpec(uri));
        }
        return specification;
      }
      cache[uri] = null;
      String json;
      try {
        json = await read(uri);
      } catch (e) {
        throw new LibrariesSpecificationException(
          messageIncludePathCouldNotBeRead(uri, e),
        );
      }
      return cache[uri] = await LibrariesSpecification.parse(
        uri,
        json,
        loadSpecification,
      );
    }

    return loadSpecification(uri);
  }

  /// Parse the given [json] as a library specification, resolving any relative
  /// paths from [specUri].
  ///
  /// May throw an exception if [json] is not properly formatted or contains
  /// invalid values.
  static Future<LibrariesSpecification> parse(
    Uri specUri,
    String? json,
    Future<LibrariesSpecification> Function(Uri uri) loadSpecification,
  ) async {
    if (json == null) return new LibrariesSpecification(specUri);
    Map<String, dynamic> jsonData;
    try {
      dynamic data = jsonDecode(json);
      if (data is! Map<String, dynamic>) {
        return _reportError(messageTopLevelIsNotAMap(specUri));
      }
      jsonData = data;
    } on FormatException catch (e) {
      throw new LibrariesSpecificationException(e);
    }
    Map<String, TargetLibrariesSpecification> targets =
        <String, TargetLibrariesSpecification>{};

    Set<String> currentTargets = {};

    Future<TargetLibrariesSpecification> resolveTargetData(
      String targetName,
    ) async {
      TargetLibrariesSpecification? spec = targets[targetName];
      if (spec != null) {
        return spec;
      }
      if (currentTargets.contains(targetName)) {
        _reportError(messageCyclicInternalInclude(targetName, specUri));
      }

      currentTargets.add(targetName);
      Map<String, LibraryInfo> libraries = <String, LibraryInfo>{};
      Object? targetData = jsonData[targetName];
      if (targetData is! Map) {
        _reportError(messageTargetIsNotAMap(targetName, specUri));
      }

      Object? include = targetData["include"];
      if (include != null) {
        if (include is! List) {
          _reportError(messageIncludeIsNotAList(targetName, specUri));
        }
        for (Object? map in include) {
          if (map is! Map<String, dynamic>) {
            _reportError(messageIncludeEntryIsNotAMap(targetName, specUri));
          }
          if (!map.containsKey("target")) {
            _reportError(messageIncludeTargetMissing(targetName, specUri));
          }
          Object? target = map["target"];
          if (target is! String) {
            _reportError(messageIncludeTargetIsNotAString(targetName, specUri));
          }
          if (!map.containsKey("path")) {
            if (!jsonData.containsKey(target)) {
              _reportError(messageMissingTarget(target, specUri));
            }
            TargetLibrariesSpecification targetLibrariesSpecification =
                await resolveTargetData(target);
            libraries.addAll(targetLibrariesSpecification._libraries);
          } else {
            Object? path = map["path"];
            if (path is! String) {
              _reportError(messageIncludePathIsNotAString(targetName, specUri));
            }
            Uri uri = Uri.parse(path);
            if (uri.hasScheme && !uri.isScheme('file')) {
              return _reportError(messageUnsupportedUriScheme(path, specUri));
            }
            LibrariesSpecification specification = await loadSpecification(
              specUri.resolveUri(uri),
            );
            TargetLibrariesSpecification targetSpecification = specification
                .specificationFor(target);
            for (LibraryInfo libraryInfo in targetSpecification.allLibraries) {
              libraries[libraryInfo.name] = libraryInfo;
            }
          }
        }
      }
      if (!targetData.containsKey("libraries")) {
        _reportError(messageTargetLibrariesMissing(targetName, specUri));
      }
      Object? librariesData = targetData["libraries"];
      if (librariesData is! Map<String, dynamic>) {
        _reportError(messageLibrariesEntryIsNotAMap(targetName, specUri));
      }
      librariesData.forEach((String libraryName, Object? data) {
        if (data is! Map<String, dynamic>) {
          _reportError(
            messageLibraryDataIsNotAMap(libraryName, targetName, specUri),
          );
        }
        Uri checkAndResolve(Object? uriString) {
          if (uriString is! String) {
            return _reportError(
              messageLibraryUriIsNotAString(
                uriString,
                libraryName,
                targetName,
                specUri,
              ),
            );
          }
          Uri uri = Uri.parse(uriString);
          if (uri.hasScheme && !uri.isScheme('file')) {
            return _reportError(
              messageUnsupportedUriScheme(uriString, specUri),
            );
          }
          return specUri.resolveUri(uri);
        }

        if (!data.containsKey('uri')) {
          _reportError(
            messageLibraryUriMissing(libraryName, targetName, specUri),
          );
        }
        Uri uri = checkAndResolve(data['uri']);
        List<Uri> patches;
        if (data['patches'] is List) {
          patches = data['patches']
              .map<Uri>((s) => specUri.resolve(s))
              .toList();
        } else if (data['patches'] is String) {
          patches = [checkAndResolve(data['patches'])];
        } else if (data['patches'] == null) {
          patches = const [];
        } else {
          _reportError(messagePatchesMustBeListOrString(libraryName));
        }

        final Object supportConditionalImport =
            data['support_conditional_import'] ?? true;
        if (supportConditionalImport is! bool) {
          _reportError(
            messagePropertyIsNotABool(
              'support_conditional_import',
              supportConditionalImport,
            ),
          );
        }

        final Object? supportDirectImportRaw = data['support_direct_import'];
        final Importability? importability = supportDirectImportRaw == null
            ? Importability.always
            : Importability.fromJson(supportDirectImportRaw);
        if (importability == null) {
          _reportError(
            messageSupportDirectImportIsNotValidValue(supportDirectImportRaw!),
          );
        }

        libraries[libraryName] = new LibraryInfo(
          libraryName,
          uri,
          patches,
          // Internal libraries are never supported through conditional
          // imports and const `fromEnvironment` expressions.
          supportConditionalImport:
              supportConditionalImport && !libraryName.startsWith('_'),
          importability: importability,
        );
      });
      currentTargets.remove(targetName);
      return targets[targetName] = new TargetLibrariesSpecification(
        targetName,
        libraries,
      );
    }

    for (String targetName in jsonData.keys) {
      if (targetName.startsWith("comment:")) {
        continue;
      }
      await resolveTargetData(targetName);
    }
    return new LibrariesSpecification(specUri, targets);
  }

  static Never _reportError(String error) =>
      throw new LibrariesSpecificationException(error);

  /// Serialize this specification to json.
  ///
  /// If possible serializes paths relative to [outputUri].
  String toJsonString(Uri outputUri) => jsonEncode(toJsonMap(outputUri));

  Map toJsonMap(Uri outputUri) {
    Map result = {};
    Uri dir = outputUri.resolve('.');
    String pathFor(Uri uri) => relativizeUri(dir, uri, isWindows);
    _targets.forEach((targetName, target) {
      Map libraries = {};
      target._libraries.forEach((name, lib) {
        libraries[name] = {
          'uri': pathFor(lib.uri),
          'patches': lib.patches.map(pathFor).toList(),
        };
        if (!lib.supportConditionalImport) {
          libraries[name]['support_conditional_import'] = false;
        }
        if (lib.importability != Importability.always) {
          libraries[name]['support_direct_import'] = lib.importability.value;
        }
      });
      result[targetName] = {'libraries': libraries};
    });
    return result;
  }
}

/// Determines whether or not a `dart:*` library is importable for a given
/// platform.
enum Importability {
  /// This `dart:*` library is always importable on the target platform.
  always(value: 'always'),

  /// This `dart:*` library is only importable on the target platform when
  /// `--include-unsupported-platform-library-stubs` is provided to the CFE.
  withFlag(value: 'with_flag'),

  /// This `dart:*` library is never importable on the target platform.
  never(value: 'never');

  const Importability({required this.value});

  final String value;

  static Importability? fromJson(Object? value) {
    if (value is! String) return null;
    if (value == always.value) return always;
    if (value == withFlag.value) return withFlag;
    if (value == never.value) return never;
    return null;
  }
}

/// Specifies information about all libraries supported by a given target.
class TargetLibrariesSpecification {
  /// Name of the target platform.
  final String targetName;

  final Map<String, LibraryInfo> _libraries;

  const TargetLibrariesSpecification(
    this.targetName, [
    this._libraries = const <String, LibraryInfo>{},
  ]);

  /// Details about a library whose import is `dart:$name`.
  LibraryInfo? libraryInfoFor(String name) => _libraries[name];

  Iterable<LibraryInfo> get allLibraries => _libraries.values;
}

/// Information about a `dart:` library in a specific target platform.
class LibraryInfo {
  /// The name of the library, which is the path developers use to import this
  /// library (as `dart:$name`).
  final String name;

  /// The file defining the main implementation of the library.
  final Uri uri;

  /// Patch files used for this library in the target platform, if any.
  final List<Uri> patches;

  /// Whether the library is supported and thus `dart.library.name` is "true"
  /// for conditional imports and fromEnvironment constants.
  final bool supportConditionalImport;

  /// Whether the library is importable for a given target platform.
  ///
  /// If not explicitly provided, this field defaults to [Importability.always].
  final Importability importability;

  const LibraryInfo(
    this.name,
    this.uri,
    this.patches, {
    this.supportConditionalImport = true,
    this.importability = Importability.always,
  });

  /// The import uri for the defined library.
  Uri get importUri => Uri.parse('dart:${name}');
}

class LibrariesSpecificationException {
  Object error;
  LibrariesSpecificationException(this.error);

  @override
  String toString() => '$error';
}

String messageMissingTarget(String targetName, Uri specUri) =>
    'No library specification for target "$targetName" in ${specUri}.';

String messageCyclicSpec(Uri specUri) => 'Cyclic dependency in ${specUri}.';

String messageCyclicInternalInclude(String targetName, Uri specUri) =>
    'Cyclic dependency of target "$targetName" in ${specUri}.';

String messageTopLevelIsNotAMap(Uri specUri) =>
    'Top-level specification is not a map in ${specUri}.';

String messageTargetIsNotAMap(String targetName, Uri specUri) =>
    'Target specification for "$targetName" is not a map in $specUri.';

String messageIncludeIsNotAList(String targetName, Uri specUri) =>
    '"include" specification for "$targetName" is not a list in $specUri.';

String messageIncludeEntryIsNotAMap(String targetName, Uri specUri) =>
    '"include" entry in "$targetName" is not a map in $specUri.';

String messageIncludePathIsNotAString(String targetName, Uri specUri) =>
    '"include" path in "$targetName" is not a string in  $specUri.';

String messageIncludePathCouldNotBeRead(Uri includeUri, Object error) =>
    '"include" path \'$includeUri\' could not be read: $error';

String messageIncludeTargetMissing(String targetName, Uri specUri) =>
    '"include" target in "$targetName" is missing in $specUri.';

String messageIncludeTargetIsNotAString(String targetName, Uri specUri) =>
    '"include" target in "$targetName" is not a string in $specUri.';

String messageTargetLibrariesMissing(String targetName, Uri specUri) =>
    'Target specification '
    'for "$targetName" doesn\'t have a libraries entry in $specUri.';

String messageLibrariesEntryIsNotAMap(String targetName, Uri specUri) =>
    '"libraries" entry for "$targetName" is not a map in $specUri.';

String messageLibraryDataIsNotAMap(
  String libraryName,
  String targetName,
  Uri specUri,
) =>
    'Library data for \'$libraryName\' in target "$targetName" is not a map '
    'in $specUri.';

String messageLibraryUriMissing(
  String libraryName,
  String targetName,
  Uri specUri,
) =>
    '"uri" is missing '
    'from library \'$libraryName\' in target "$targetName" in $specUri.';

String messageLibraryUriIsNotAString(
  Object? uriValue,
  String libraryName,
  String targetName,
  Uri specUri,
) =>
    'Uri value `$uriValue` is not a string '
    '(from library \'$libraryName\' in target "$targetName" in $specUri).';

String messageUnsupportedUriScheme(String uriValue, Uri specUri) =>
    "Uri scheme in '$uriValue' is not supported in $specUri.";

String messagePatchesMustBeListOrString(String libraryName) =>
    '"patches" entry for "$libraryName" is not a list or a string.';

String messagePropertyIsNotABool(String key, Object value) =>
    '"$key" entry: expected a `bool` but '
    'got a `${value.runtimeType}` ("$value").';

String messageOnlyOnePropertyCanBeDefined(List<String> properties) {
  return 'Only one of the following properties can be defined: '
      '${properties.map((e) => '`$e`').join(',')}.';
}

String messageSupportDirectImportIsNotValidValue(Object value) {
  final String values = Importability.values
      .map((e) => '`${e.value}`')
      .join(',');
  return '"support_direct_import" entry: expected one of $values but got a '
      '`${value.runtimeType}` ("$value").';
}
