// 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.

/// Module metadata format version
///
/// Module reader always creates the current version but is able to read
/// metadata files with later versions as long as the changes are backward
/// compatible, i.e. only minor or patch versions have changed.
///
/// See: https://goto.google.com/dart-web-debugger-metadata
class ModuleMetadataVersion {
  final int majorVersion;
  final int minorVersion;
  final int patchVersion;

  const ModuleMetadataVersion(
    this.majorVersion,
    this.minorVersion,
    this.patchVersion,
  );

  /// Current metadata version
  ///
  /// Version follows simple semantic versioning format 'major.minor.patch'
  /// See https://semver.org
  static const ModuleMetadataVersion current = ModuleMetadataVersion(2, 0, 0);

  /// Previous version supported by the metadata reader
  static const ModuleMetadataVersion previous = ModuleMetadataVersion(1, 0, 0);

  /// Current metadata version created by the reader
  String get version => '$majorVersion.$minorVersion.$patchVersion';

  /// Is this metadata version compatible with the given version
  ///
  /// The minor and patch version changes never remove any fields that current
  /// version supports, so the reader can create current metadata version from
  /// any file created with a later writer, as long as the major version does
  /// not change.
  bool isCompatibleWith(String version) {
    final parts = version.split('.');
    if (parts.length != 3) {
      throw FormatException(
        'Version: $version'
        'does not follow simple semantic versioning format',
      );
    }
    final major = int.parse(parts[0]);
    final minor = int.parse(parts[1]);
    final patch = int.parse(parts[2]);
    return major == majorVersion &&
        minor >= minorVersion &&
        patch >= patchVersion;
  }
}

/// Library metadata
///
/// Represents library metadata used in the debugger,
/// supports reading from and writing to json
/// See: https://goto.google.com/dart-web-debugger-metadata
class LibraryMetadata {
  /// Library name as defined in pubspec.yaml
  final String name;

  /// Library importUri
  ///
  /// Example package:path/path.dart
  final String importUri;

  /// All file uris from the library
  ///
  /// Can be relative paths to the directory of the fileUri
  final List<String> partUris;

  LibraryMetadata(this.name, this.importUri, this.partUris);

  LibraryMetadata.fromJson(Map<String, dynamic> json)
    : name = _readRequiredField(json, 'name'),
      importUri = _readRequiredField(json, 'importUri'),
      partUris = _readOptionalList(json, 'partUris') ?? [];

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'importUri': importUri,
      'partUris': [...partUris],
    };
  }
}

/// Module metadata
///
/// Represents module metadata used in the debugger,
/// supports reading from and writing to json
/// See: https://goto.google.com/dart-web-debugger-metadata
class ModuleMetadata {
  /// Metadata format version
  late final String version;

  /// Module name
  ///
  /// Used as a name of the js module created by the compiler and
  /// as key to store and load modules in the debugger and the browser
  // TODO(srujzs): Remove once https://github.com/dart-lang/sdk/issues/59618 is
  // resolved.
  final String name;

  /// Name of the function enclosing the module
  ///
  /// Used by debugger to determine the top dart scope
  final String closureName;

  /// Source map uri
  final String sourceMapUri;

  /// Module uri
  final String moduleUri;

  final Map<String, LibraryMetadata> libraries = {};

  ModuleMetadata(
    this.name,
    this.closureName,
    this.sourceMapUri,
    this.moduleUri, {
    String? ver,
  }) {
    version = ver ?? ModuleMetadataVersion.current.version;
  }

  /// Add [library] to metadata
  ///
  /// Used for filling the metadata in the compiler or for reading from
  /// stored metadata files.
  void addLibrary(LibraryMetadata library) {
    if (!libraries.containsKey(library.importUri)) {
      libraries[library.importUri] = library;
    } else {
      throw Exception(
        'Metadata creation error: '
        'Cannot add library $library with uri ${library.importUri}: '
        'another library "${libraries[library.importUri]}" is found '
        'with the same uri',
      );
    }
  }

  ModuleMetadata.fromJson(Map<String, dynamic> json)
    : version = _readRequiredField(json, 'version'),
      name = _readRequiredField(json, 'name'),
      closureName = _readRequiredField(json, 'closureName'),
      sourceMapUri = _readRequiredField(json, 'sourceMapUri'),
      moduleUri = _readRequiredField(json, 'moduleUri') {
    if (!ModuleMetadataVersion.current.isCompatibleWith(version) &&
        !ModuleMetadataVersion.previous.isCompatibleWith(version)) {
      throw Exception(
        'Unsupported metadata version $version. '
        '\n  Supported versions: '
        '\n    ${ModuleMetadataVersion.current.version} '
        '\n    ${ModuleMetadataVersion.previous.version}',
      );
    }

    for (final l in _readRequiredList(json, 'libraries')) {
      addLibrary(LibraryMetadata.fromJson(l as Map<String, dynamic>));
    }
  }

  Map<String, dynamic> toJson() {
    return {
      'version': version,
      'name': name,
      'closureName': closureName,
      'sourceMapUri': sourceMapUri,
      'moduleUri': moduleUri,
      'libraries': [for (final lib in libraries.values) lib.toJson()],
    };
  }
}

T _readRequiredField<T>(Map<String, dynamic> json, String field) {
  if (!json.containsKey(field)) {
    throw FormatException('Required field $field is not set in $json');
  }
  return json[field]! as T;
}

T? _readOptionalField<T>(Map<String, dynamic> json, String field) =>
    json[field] as T?;

List<T> _readRequiredList<T>(Map<String, dynamic> json, String field) {
  final list = _readRequiredField<List<dynamic>>(json, field);
  return List.castFrom<dynamic, T>(list);
}

List<T>? _readOptionalList<T>(Map<String, dynamic> json, String field) {
  final list = _readOptionalField<List<dynamic>>(json, field);
  return list == null ? null : List.castFrom<dynamic, T>(list);
}
